mac-human-design 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/LICENSE +21 -0
- package/README.md +92 -0
- package/package.json +59 -0
- package/src/components/AppWindowShell.tsx +25 -0
- package/src/components/MacSegmentedControl.tsx +54 -0
- package/src/components/StatusMessage.tsx +52 -0
- package/src/components/SymbolIconButton.tsx +138 -0
- package/src/components/ViewDragRegion.tsx +18 -0
- package/src/components/index.ts +5 -0
- package/src/index.ts +18 -0
- package/src/symbols/SystemSymbolImage.tsx +72 -0
- package/src/symbols/index.ts +9 -0
- package/src/symbols/systemSymbolService.ts +68 -0
- package/src/symbols/useSystemSymbol.ts +75 -0
- package/src/tauri/index.ts +4 -0
- package/src/theme/index.ts +1 -0
- package/src/theme/macosTheme.ts +570 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/isDraggableAreaTarget.ts +22 -0
- package/src-tauri/Cargo.toml +21 -0
- package/src-tauri/src/lib.rs +10 -0
- package/src-tauri/src/system_symbols.rs +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is furnished
|
|
10
|
+
to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# human-design
|
|
2
|
+
|
|
3
|
+
A reusable shared library for Tauri macOS apps. It includes:
|
|
4
|
+
|
|
5
|
+
- `MacSegmentedControl` and macOS-focused BaseUI theme tokens.
|
|
6
|
+
- A reusable SF Symbols fetch layer with React hooks/components.
|
|
7
|
+
- A Rust/Tauri plugin crate that exposes a native command:
|
|
8
|
+
`system_symbol_png_data_url`.
|
|
9
|
+
|
|
10
|
+
## Structure
|
|
11
|
+
|
|
12
|
+
- `src/` - TypeScript/React library exports
|
|
13
|
+
- `src-tauri/` - Rust plugin crate (`human-design-tauri-system-symbols`)
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
From the consumer app:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm i human-design
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
In the Tauri Rust side, add the local path dependency while developing:
|
|
24
|
+
|
|
25
|
+
```toml
|
|
26
|
+
[dependencies]
|
|
27
|
+
human-design-tauri-system-symbols = { path = "../human-design/src-tauri" }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then register the plugin in your `src-tauri/src/lib.rs`:
|
|
31
|
+
|
|
32
|
+
```rust
|
|
33
|
+
fn main() {
|
|
34
|
+
tauri::Builder::default()
|
|
35
|
+
.plugin(human_design_tauri_system_symbols::init())
|
|
36
|
+
.run(tauri::generate_context!())
|
|
37
|
+
.expect("error while running app");
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage (React)
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { useState } from "react";
|
|
45
|
+
import { MacSegmentedControl } from "human-design";
|
|
46
|
+
import { SystemSymbolImage } from "human-design/symbols";
|
|
47
|
+
|
|
48
|
+
export function Demo() {
|
|
49
|
+
const [tab, setTab] = useState<"files" | "settings">("files");
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div>
|
|
53
|
+
<SystemSymbolImage symbolName="folder" sizePx={18} fallback="▢" />
|
|
54
|
+
<MacSegmentedControl
|
|
55
|
+
options={[
|
|
56
|
+
{ value: "files", label: "Files" },
|
|
57
|
+
{ value: "settings", label: "Settings" },
|
|
58
|
+
]}
|
|
59
|
+
value={tab}
|
|
60
|
+
onChange={setTab}
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Why it works cross-platform
|
|
68
|
+
|
|
69
|
+
## Shared UI components moved in
|
|
70
|
+
|
|
71
|
+
The following components are now in the shared library and can be imported by any app:
|
|
72
|
+
|
|
73
|
+
- `AppWindowShell`
|
|
74
|
+
- `StatusMessage`
|
|
75
|
+
- `SymbolIconButton`
|
|
76
|
+
- `ViewDragRegion`
|
|
77
|
+
- `isDraggableAreaTarget` utility
|
|
78
|
+
|
|
79
|
+
Example import:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import {
|
|
83
|
+
AppWindowShell,
|
|
84
|
+
StatusMessage,
|
|
85
|
+
SymbolIconButton,
|
|
86
|
+
ViewDragRegion,
|
|
87
|
+
isDraggableAreaTarget,
|
|
88
|
+
} from "human-design";
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- On macOS, Rust command renders the symbol to PNG via `NSImage`.
|
|
92
|
+
- On non-macOS, command returns `None` so invocations stay safe and predictable.
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mac-human-design",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Reusable macOS-oriented UI primitives and SF Symbols bridge for Tauri apps.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./components": "./src/components/index.ts",
|
|
11
|
+
"./theme": "./src/theme/index.ts",
|
|
12
|
+
"./symbols": "./src/symbols/index.ts",
|
|
13
|
+
"./tauri": "./src/tauri/index.ts",
|
|
14
|
+
"./utils": "./src/utils/index.ts",
|
|
15
|
+
"./rust": "./src-tauri"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src/**/*",
|
|
19
|
+
"src-tauri/**/*",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"keywords": [
|
|
24
|
+
"tauri",
|
|
25
|
+
"tauri-plugin",
|
|
26
|
+
"macos",
|
|
27
|
+
"sf-symbols",
|
|
28
|
+
"react",
|
|
29
|
+
"components"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"publish": "npm publish --access public"
|
|
33
|
+
},
|
|
34
|
+
"author": "human-design",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@tauri-apps/api": "^2",
|
|
38
|
+
"baseui": "^16.1.1",
|
|
39
|
+
"react": "^18 || ^19",
|
|
40
|
+
"react-dom": "^18 || ^19",
|
|
41
|
+
"styletron-react": "^6.1.1",
|
|
42
|
+
"styletron-standard": "^4.0.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"@tauri-apps/api": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"baseui": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"styletron-react": {
|
|
52
|
+
"optional": true
|
|
53
|
+
},
|
|
54
|
+
"styletron-standard": {
|
|
55
|
+
"optional": true
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {}
|
|
59
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { Block } from "baseui/block";
|
|
3
|
+
import { useStyletron } from "baseui";
|
|
4
|
+
|
|
5
|
+
export function AppWindowShell({ children }: { children: ReactNode }) {
|
|
6
|
+
const [, theme] = useStyletron();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<Block
|
|
10
|
+
width="100%"
|
|
11
|
+
$style={{
|
|
12
|
+
minHeight: "100vh",
|
|
13
|
+
boxSizing: "border-box",
|
|
14
|
+
display: "flex",
|
|
15
|
+
flexDirection: "column",
|
|
16
|
+
backgroundColor: theme.colors.backgroundPrimary,
|
|
17
|
+
color: theme.colors.contentPrimary,
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
<Block flex="1" $style={{ minHeight: 0, display: "flex", flexDirection: "column" }}>
|
|
21
|
+
{children}
|
|
22
|
+
</Block>
|
|
23
|
+
</Block>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { FILL, Segment, SegmentedControl } from "baseui/segmented-control";
|
|
3
|
+
import {
|
|
4
|
+
macosSegmentedControlOverrides,
|
|
5
|
+
macosSegmentOverrides,
|
|
6
|
+
} from "../theme/macosTheme";
|
|
7
|
+
|
|
8
|
+
export type MacSegmentedControlOption<T extends string> = {
|
|
9
|
+
value: T;
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type MacSegmentedControlProps<T extends string> = {
|
|
15
|
+
options: readonly MacSegmentedControlOption<T>[];
|
|
16
|
+
value: T;
|
|
17
|
+
onChange: (value: T) => void;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
fullWidth?: boolean;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function MacSegmentedControl<T extends string>({
|
|
23
|
+
options,
|
|
24
|
+
value,
|
|
25
|
+
onChange,
|
|
26
|
+
disabled,
|
|
27
|
+
fullWidth,
|
|
28
|
+
}: MacSegmentedControlProps<T>) {
|
|
29
|
+
return (
|
|
30
|
+
<SegmentedControl
|
|
31
|
+
activeKey={value}
|
|
32
|
+
disabled={disabled}
|
|
33
|
+
fill={FILL.fixed}
|
|
34
|
+
onChange={({ activeKey }) => {
|
|
35
|
+
const nextOption = options.find((option) => option.value === activeKey);
|
|
36
|
+
if (!nextOption) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
onChange(nextOption.value);
|
|
40
|
+
}}
|
|
41
|
+
overrides={macosSegmentedControlOverrides}
|
|
42
|
+
width={fullWidth ? "100%" : undefined}
|
|
43
|
+
>
|
|
44
|
+
{options.map((option) => (
|
|
45
|
+
<Segment
|
|
46
|
+
key={option.value}
|
|
47
|
+
label={option.label}
|
|
48
|
+
disabled={option.disabled}
|
|
49
|
+
overrides={macosSegmentOverrides}
|
|
50
|
+
/>
|
|
51
|
+
))}
|
|
52
|
+
</SegmentedControl>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import { Block } from "baseui/block";
|
|
3
|
+
import { useStyletron } from "baseui";
|
|
4
|
+
|
|
5
|
+
type StatusMessageProps = {
|
|
6
|
+
intent: "error" | "success" | "info";
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function StatusMessage({ intent, message }: StatusMessageProps) {
|
|
11
|
+
const [, theme] = useStyletron();
|
|
12
|
+
const isDark = (theme.name ?? "").toLowerCase().includes("dark");
|
|
13
|
+
|
|
14
|
+
const palette =
|
|
15
|
+
intent === "error"
|
|
16
|
+
? {
|
|
17
|
+
background: isDark ? "rgba(255, 84, 89, 0.16)" : "rgba(255, 84, 89, 0.12)",
|
|
18
|
+
border: isDark ? "rgba(255, 84, 89, 0.45)" : "rgba(255, 84, 89, 0.35)",
|
|
19
|
+
text: isDark ? "#ff9ea2" : "#8f1c1f",
|
|
20
|
+
}
|
|
21
|
+
: intent === "success"
|
|
22
|
+
? {
|
|
23
|
+
background: isDark ? "rgba(52, 199, 89, 0.16)" : "rgba(52, 199, 89, 0.12)",
|
|
24
|
+
border: isDark ? "rgba(52, 199, 89, 0.45)" : "rgba(52, 199, 89, 0.3)",
|
|
25
|
+
text: isDark ? "#8ff0ad" : "#0f6b2b",
|
|
26
|
+
}
|
|
27
|
+
: {
|
|
28
|
+
background: isDark ? "rgba(10, 132, 255, 0.16)" : "rgba(10, 132, 255, 0.08)",
|
|
29
|
+
border: isDark ? "rgba(10, 132, 255, 0.45)" : "rgba(10, 132, 255, 0.25)",
|
|
30
|
+
text: isDark ? "#9fcbff" : "#0b4e98",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Block
|
|
35
|
+
marginTop="scale400"
|
|
36
|
+
paddingTop="scale300"
|
|
37
|
+
paddingRight="scale400"
|
|
38
|
+
paddingBottom="scale300"
|
|
39
|
+
paddingLeft="scale400"
|
|
40
|
+
$style={{
|
|
41
|
+
backgroundColor: palette.background,
|
|
42
|
+
border: `1px solid ${palette.border}`,
|
|
43
|
+
borderRadius: "10px",
|
|
44
|
+
color: palette.text,
|
|
45
|
+
fontSize: "13px",
|
|
46
|
+
lineHeight: "1.35",
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{message}
|
|
50
|
+
</Block>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { invoke } from "@tauri-apps/api/core";
|
|
3
|
+
import { Button, KIND, SIZE } from "baseui/button";
|
|
4
|
+
import { StatefulTooltip } from "baseui/tooltip";
|
|
5
|
+
import { Block } from "baseui/block";
|
|
6
|
+
|
|
7
|
+
const SYMBOL_SCALE_MULTIPLIER = 2.5;
|
|
8
|
+
const SYMBOL_MIN_POINT_SIZE = 10;
|
|
9
|
+
const SYMBOL_MAX_POINT_SIZE = 160;
|
|
10
|
+
const DEFAULT_SYMBOL_POINT_SIZE = 18;
|
|
11
|
+
|
|
12
|
+
const systemSymbolIconCache = new Map<string, string | null>();
|
|
13
|
+
|
|
14
|
+
function getSymbolPointSize(cssPx: number): number {
|
|
15
|
+
const pixelRatio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
|
|
16
|
+
return Math.round(Math.max(SYMBOL_MIN_POINT_SIZE, Math.min(SYMBOL_MAX_POINT_SIZE, cssPx * pixelRatio * SYMBOL_SCALE_MULTIPLIER)));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function symbolCacheKey(symbolName: string, pointSize: number) {
|
|
20
|
+
return `${symbolName}@${pointSize}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type SymbolIconButtonProps = {
|
|
24
|
+
label: string;
|
|
25
|
+
symbolName: string;
|
|
26
|
+
fallback: string;
|
|
27
|
+
onClick: () => void;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
isLoading?: boolean;
|
|
30
|
+
iconSizePx?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ICON_BUTTON_SIZE = "30px";
|
|
34
|
+
const ICON_BUTTON_RADIUS = "7px";
|
|
35
|
+
|
|
36
|
+
export function SymbolIconButton({
|
|
37
|
+
label,
|
|
38
|
+
symbolName,
|
|
39
|
+
fallback,
|
|
40
|
+
onClick,
|
|
41
|
+
disabled,
|
|
42
|
+
isLoading,
|
|
43
|
+
iconSizePx = 18,
|
|
44
|
+
}: SymbolIconButtonProps) {
|
|
45
|
+
const symbolPointSize = getSymbolPointSize(iconSizePx);
|
|
46
|
+
const symbolCacheLookupKey = symbolCacheKey(symbolName, symbolPointSize);
|
|
47
|
+
|
|
48
|
+
const [symbolDataUrl, setSymbolDataUrl] = useState<string | null>(() => {
|
|
49
|
+
return systemSymbolIconCache.get(symbolCacheLookupKey) ?? null;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const cachedDataUrl = systemSymbolIconCache.get(symbolCacheLookupKey);
|
|
54
|
+
if (cachedDataUrl !== undefined) {
|
|
55
|
+
setSymbolDataUrl(cachedDataUrl);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let isMounted = true;
|
|
60
|
+
void invoke<string | null>("system_symbol_png_data_url", {
|
|
61
|
+
name: symbolName,
|
|
62
|
+
point_size: symbolPointSize,
|
|
63
|
+
})
|
|
64
|
+
.then((dataUrl) => {
|
|
65
|
+
systemSymbolIconCache.set(symbolCacheLookupKey, dataUrl);
|
|
66
|
+
if (isMounted) {
|
|
67
|
+
setSymbolDataUrl(dataUrl);
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.catch(() => {
|
|
71
|
+
systemSymbolIconCache.set(symbolCacheLookupKey, null);
|
|
72
|
+
if (isMounted) {
|
|
73
|
+
setSymbolDataUrl(null);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return () => {
|
|
78
|
+
isMounted = false;
|
|
79
|
+
};
|
|
80
|
+
}, [symbolName, symbolPointSize, symbolCacheLookupKey]);
|
|
81
|
+
|
|
82
|
+
const glyphPx = `${iconSizePx}px`;
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<StatefulTooltip content={label}>
|
|
86
|
+
<Button
|
|
87
|
+
aria-label={label}
|
|
88
|
+
title={label}
|
|
89
|
+
kind={KIND.tertiary}
|
|
90
|
+
size={SIZE.compact}
|
|
91
|
+
isLoading={isLoading}
|
|
92
|
+
disabled={disabled}
|
|
93
|
+
onClick={onClick}
|
|
94
|
+
overrides={{
|
|
95
|
+
BaseButton: {
|
|
96
|
+
style: {
|
|
97
|
+
width: ICON_BUTTON_SIZE,
|
|
98
|
+
minWidth: ICON_BUTTON_SIZE,
|
|
99
|
+
height: ICON_BUTTON_SIZE,
|
|
100
|
+
minHeight: ICON_BUTTON_SIZE,
|
|
101
|
+
borderRadius: ICON_BUTTON_RADIUS,
|
|
102
|
+
paddingTop: 0,
|
|
103
|
+
paddingRight: 0,
|
|
104
|
+
paddingBottom: 0,
|
|
105
|
+
paddingLeft: 0,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
<Block
|
|
111
|
+
as="span"
|
|
112
|
+
aria-hidden
|
|
113
|
+
$style={{
|
|
114
|
+
display: "block",
|
|
115
|
+
width: glyphPx,
|
|
116
|
+
height: glyphPx,
|
|
117
|
+
color: "currentColor",
|
|
118
|
+
backgroundColor: symbolDataUrl ? "currentColor" : "transparent",
|
|
119
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
|
|
120
|
+
fontSize: symbolDataUrl ? "0" : glyphPx,
|
|
121
|
+
fontWeight: 500,
|
|
122
|
+
lineHeight: "1",
|
|
123
|
+
WebkitMaskImage: symbolDataUrl ? `url("${symbolDataUrl}")` : undefined,
|
|
124
|
+
WebkitMaskPosition: symbolDataUrl ? "center" : undefined,
|
|
125
|
+
WebkitMaskRepeat: symbolDataUrl ? "no-repeat" : undefined,
|
|
126
|
+
WebkitMaskSize: symbolDataUrl ? "contain" : undefined,
|
|
127
|
+
maskImage: symbolDataUrl ? `url("${symbolDataUrl}")` : undefined,
|
|
128
|
+
maskPosition: symbolDataUrl ? "center" : undefined,
|
|
129
|
+
maskRepeat: symbolDataUrl ? "no-repeat" : undefined,
|
|
130
|
+
maskSize: symbolDataUrl ? "contain" : undefined,
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{symbolDataUrl ? null : fallback}
|
|
134
|
+
</Block>
|
|
135
|
+
</Button>
|
|
136
|
+
</StatefulTooltip>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Block } from "baseui/block";
|
|
2
|
+
|
|
3
|
+
type ViewDragRegionProps = {
|
|
4
|
+
height?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function ViewDragRegion({ height = "32px" }: ViewDragRegionProps) {
|
|
8
|
+
return (
|
|
9
|
+
<Block
|
|
10
|
+
data-tauri-drag-region
|
|
11
|
+
$style={{
|
|
12
|
+
height,
|
|
13
|
+
minHeight: height,
|
|
14
|
+
flexShrink: 0,
|
|
15
|
+
}}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { MacSegmentedControl, type MacSegmentedControlOption } from "./MacSegmentedControl";
|
|
2
|
+
export { StatusMessage } from "./StatusMessage";
|
|
3
|
+
export { SymbolIconButton } from "./SymbolIconButton";
|
|
4
|
+
export { AppWindowShell } from "./AppWindowShell";
|
|
5
|
+
export { ViewDragRegion } from "./ViewDragRegion";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { AppWindowShell } from "./components/AppWindowShell";
|
|
2
|
+
export { MacSegmentedControl, type MacSegmentedControlOption } from "./components/MacSegmentedControl";
|
|
3
|
+
export { SymbolIconButton } from "./components/SymbolIconButton";
|
|
4
|
+
export { StatusMessage } from "./components/StatusMessage";
|
|
5
|
+
export { ViewDragRegion } from "./components/ViewDragRegion";
|
|
6
|
+
export * as macosTheme from "./theme/macosTheme";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
getSystemSymbolDataUrl,
|
|
10
|
+
clearSystemSymbolCache,
|
|
11
|
+
buildSystemSymbolCacheKey,
|
|
12
|
+
calculateSystemSymbolPointSize,
|
|
13
|
+
type SystemSymbolFetchOptions,
|
|
14
|
+
} from "./symbols/systemSymbolService";
|
|
15
|
+
export { SystemSymbolImage, type SystemSymbolImageProps } from "./symbols/SystemSymbolImage";
|
|
16
|
+
export { useSystemSymbol } from "./symbols/useSystemSymbol";
|
|
17
|
+
|
|
18
|
+
export { isDraggableAreaTarget } from "./utils/isDraggableAreaTarget";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { CSSProperties } from "react";
|
|
3
|
+
import { useSystemSymbol } from "./useSystemSymbol";
|
|
4
|
+
|
|
5
|
+
export type SystemSymbolImageProps = {
|
|
6
|
+
symbolName: string;
|
|
7
|
+
fallback?: string;
|
|
8
|
+
sizePx?: number;
|
|
9
|
+
ariaLabel?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
style?: CSSProperties;
|
|
12
|
+
pointSize?: number;
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function SystemSymbolImage({
|
|
17
|
+
symbolName,
|
|
18
|
+
fallback = "?",
|
|
19
|
+
sizePx = 18,
|
|
20
|
+
ariaLabel,
|
|
21
|
+
className,
|
|
22
|
+
style,
|
|
23
|
+
pointSize,
|
|
24
|
+
enabled = true,
|
|
25
|
+
}: SystemSymbolImageProps) {
|
|
26
|
+
const { dataUrl, loading } = useSystemSymbol({
|
|
27
|
+
symbolName,
|
|
28
|
+
cssPixelSize: sizePx,
|
|
29
|
+
options: { fallbackPointSize: pointSize },
|
|
30
|
+
enabled,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const sharedStyles = useMemo<CSSProperties>(() => {
|
|
34
|
+
const pixels = `${sizePx}px`;
|
|
35
|
+
return {
|
|
36
|
+
display: "inline-flex",
|
|
37
|
+
width: pixels,
|
|
38
|
+
height: pixels,
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
justifyContent: "center",
|
|
41
|
+
flexShrink: 0,
|
|
42
|
+
...style,
|
|
43
|
+
};
|
|
44
|
+
}, [sizePx, style]);
|
|
45
|
+
|
|
46
|
+
if (!enabled) {
|
|
47
|
+
return <span className={className} style={sharedStyles} aria-label={ariaLabel} />;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (loading) {
|
|
51
|
+
return <span className={className} style={sharedStyles} aria-label={ariaLabel} />;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!dataUrl) {
|
|
55
|
+
return (
|
|
56
|
+
<span
|
|
57
|
+
className={className}
|
|
58
|
+
style={{
|
|
59
|
+
...sharedStyles,
|
|
60
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
|
|
61
|
+
fontSize: `${sizePx}px`,
|
|
62
|
+
lineHeight: "1",
|
|
63
|
+
}}
|
|
64
|
+
aria-label={ariaLabel}
|
|
65
|
+
>
|
|
66
|
+
{fallback}
|
|
67
|
+
</span>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return <img className={className} src={dataUrl} alt={ariaLabel ?? symbolName} style={sharedStyles} />;
|
|
72
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export {
|
|
2
|
+
getSystemSymbolDataUrl,
|
|
3
|
+
clearSystemSymbolCache,
|
|
4
|
+
buildSystemSymbolCacheKey,
|
|
5
|
+
calculateSystemSymbolPointSize,
|
|
6
|
+
type SystemSymbolFetchOptions,
|
|
7
|
+
} from "./systemSymbolService";
|
|
8
|
+
export { SystemSymbolImage, type SystemSymbolImageProps } from "./SystemSymbolImage";
|
|
9
|
+
export { useSystemSymbol } from "./useSystemSymbol";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { invoke } from "@tauri-apps/api/core";
|
|
2
|
+
|
|
3
|
+
export type SystemSymbolFetchOptions = {
|
|
4
|
+
pointSize?: number;
|
|
5
|
+
fallbackPointSize?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const SYSTEM_SYMBOL_CACHE = new Map<string, string | null>();
|
|
9
|
+
|
|
10
|
+
const DEFAULT_POINT_SIZE = 18;
|
|
11
|
+
const MIN_POINT_SIZE = 10;
|
|
12
|
+
const MAX_POINT_SIZE = 256;
|
|
13
|
+
const SYMBOL_SCALE_MULTIPLIER = 2.5;
|
|
14
|
+
|
|
15
|
+
export function calculateSystemSymbolPointSize(
|
|
16
|
+
cssPx: number,
|
|
17
|
+
options: { fallbackPointSize?: number } = {},
|
|
18
|
+
): number {
|
|
19
|
+
const fallback = options.fallbackPointSize ?? DEFAULT_POINT_SIZE;
|
|
20
|
+
const rawPointSize = Math.max(1, Math.floor(cssPx * SYMBOL_SCALE_MULTIPLIER));
|
|
21
|
+
const devicePixelRatio =
|
|
22
|
+
typeof window === "undefined" ? 1 : Math.max(1, window.devicePixelRatio ?? 1);
|
|
23
|
+
|
|
24
|
+
if (!Number.isFinite(rawPointSize) || rawPointSize <= 0) {
|
|
25
|
+
return clampPointSize(fallback * devicePixelRatio);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return clampPointSize(Math.round(rawPointSize * devicePixelRatio));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clampPointSize(value: number): number {
|
|
32
|
+
return Math.min(MAX_POINT_SIZE, Math.max(MIN_POINT_SIZE, Math.round(value)));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildSystemSymbolCacheKey(symbolName: string, pointSize: number): string {
|
|
36
|
+
return `${symbolName}@${pointSize}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function clearSystemSymbolCache(): void {
|
|
40
|
+
SYSTEM_SYMBOL_CACHE.clear();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function getSystemSymbolDataUrl(
|
|
44
|
+
name: string,
|
|
45
|
+
options: SystemSymbolFetchOptions = {},
|
|
46
|
+
): Promise<string | null> {
|
|
47
|
+
const pointSize = options.pointSize;
|
|
48
|
+
const resolvedPointSize = pointSize ?? DEFAULT_POINT_SIZE;
|
|
49
|
+
const key = buildSystemSymbolCacheKey(name, resolvedPointSize);
|
|
50
|
+
|
|
51
|
+
if (SYSTEM_SYMBOL_CACHE.has(key)) {
|
|
52
|
+
return SYSTEM_SYMBOL_CACHE.get(key) ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const maybeDataUrl = await invoke<string | null>("system_symbol_png_data_url", {
|
|
57
|
+
name,
|
|
58
|
+
point_size: pointSize,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const dataUrl = maybeDataUrl ?? null;
|
|
62
|
+
SYSTEM_SYMBOL_CACHE.set(key, dataUrl);
|
|
63
|
+
return dataUrl;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
SYSTEM_SYMBOL_CACHE.set(key, null);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|