mac-human-design 0.1.10 → 0.1.12

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/changelog.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  This file tracks changes between published npm versions of `mac-human-design`.
4
4
 
5
+ ## 0.1.12
6
+
7
+ ### Fixed
8
+
9
+ - Rendered `SymbolIconButton` glyphs as direct children of the shared
10
+ `MacIconButton` render element, avoiding tooltip slot child placement
11
+ ambiguity.
12
+ - Tightened icon-button centering with explicit grid content centering,
13
+ zero line-height, vertical alignment, and grid-area placement for symbol
14
+ glyphs.
15
+
16
+ ### Changed
17
+
18
+ - Bumped the package to `0.1.12`.
19
+
20
+ ## 0.1.11
21
+
22
+ ### Fixed
23
+
24
+ - Trimmed transparent padding from generated SF Symbol PNGs before caching them,
25
+ so icon buttons center the visible glyph instead of the symbol image's
26
+ asymmetric advance box.
27
+ - Routed `SymbolIconButton` through the shared system-symbol hook so icon
28
+ buttons and standalone symbol images share the same normalized symbol data.
29
+
30
+ ### Changed
31
+
32
+ - Bumped the package to `0.1.11`.
33
+
5
34
  ## 0.1.10
6
35
 
7
36
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mac-human-design",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Reusable macOS-oriented UI primitives and SF Symbols bridge for Tauri apps.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,21 +1,5 @@
1
- import { useEffect, useState } from "react";
2
- import { invoke } from "@tauri-apps/api/core";
3
1
  import { MacIconButton, MacTooltip } from "./MacBaseUI";
4
-
5
- const SYMBOL_SCALE_MULTIPLIER = 2.5;
6
- const SYMBOL_MIN_POINT_SIZE = 10;
7
- const SYMBOL_MAX_POINT_SIZE = 160;
8
-
9
- const systemSymbolIconCache = new Map<string, string | null>();
10
-
11
- function getSymbolPointSize(cssPx: number): number {
12
- const pixelRatio = typeof window === "undefined" ? 1 : window.devicePixelRatio || 1;
13
- return Math.round(Math.max(SYMBOL_MIN_POINT_SIZE, Math.min(SYMBOL_MAX_POINT_SIZE, cssPx * pixelRatio * SYMBOL_SCALE_MULTIPLIER)));
14
- }
15
-
16
- function symbolCacheKey(symbolName: string, pointSize: number) {
17
- return `${symbolName}@${pointSize}`;
18
- }
2
+ import { useSystemSymbol } from "../symbols/useSystemSymbol";
19
3
 
20
4
  type SymbolIconButtonProps = {
21
5
  label: string;
@@ -39,43 +23,10 @@ export function SymbolIconButton({
39
23
  isLoading,
40
24
  iconSizePx = 18,
41
25
  }: SymbolIconButtonProps) {
42
- const symbolPointSize = getSymbolPointSize(iconSizePx);
43
- const symbolCacheLookupKey = symbolCacheKey(symbolName, symbolPointSize);
44
-
45
- const [symbolDataUrl, setSymbolDataUrl] = useState<string | null>(() => {
46
- return systemSymbolIconCache.get(symbolCacheLookupKey) ?? null;
26
+ const { dataUrl: symbolDataUrl } = useSystemSymbol({
27
+ symbolName,
28
+ cssPixelSize: iconSizePx,
47
29
  });
48
-
49
- useEffect(() => {
50
- const cachedDataUrl = systemSymbolIconCache.get(symbolCacheLookupKey);
51
- if (cachedDataUrl !== undefined) {
52
- setSymbolDataUrl(cachedDataUrl);
53
- return;
54
- }
55
-
56
- let isMounted = true;
57
- void invoke<string | null>("system_symbol_png_data_url", {
58
- name: symbolName,
59
- point_size: symbolPointSize,
60
- })
61
- .then((dataUrl) => {
62
- systemSymbolIconCache.set(symbolCacheLookupKey, dataUrl);
63
- if (isMounted) {
64
- setSymbolDataUrl(dataUrl);
65
- }
66
- })
67
- .catch(() => {
68
- systemSymbolIconCache.set(symbolCacheLookupKey, null);
69
- if (isMounted) {
70
- setSymbolDataUrl(null);
71
- }
72
- });
73
-
74
- return () => {
75
- isMounted = false;
76
- };
77
- }, [symbolName, symbolPointSize, symbolCacheLookupKey]);
78
-
79
30
  const glyphPx = `${iconSizePx}px`;
80
31
 
81
32
  return (
@@ -96,34 +47,34 @@ export function SymbolIconButton({
96
47
  minHeight: ICON_BUTTON_SIZE,
97
48
  borderRadius: ICON_BUTTON_RADIUS,
98
49
  }}
99
- />
50
+ >
51
+ <span
52
+ aria-hidden
53
+ className="hd-mac-symbol-icon"
54
+ style={{
55
+ width: glyphPx,
56
+ height: glyphPx,
57
+ color: "currentColor",
58
+ backgroundColor: symbolDataUrl ? "currentColor" : "transparent",
59
+ fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
60
+ fontSize: symbolDataUrl ? "0" : glyphPx,
61
+ fontWeight: 500,
62
+ lineHeight: "1",
63
+ WebkitMaskImage: symbolDataUrl ? `url("${symbolDataUrl}")` : undefined,
64
+ WebkitMaskPosition: symbolDataUrl ? "center" : undefined,
65
+ WebkitMaskRepeat: symbolDataUrl ? "no-repeat" : undefined,
66
+ WebkitMaskSize: symbolDataUrl ? "contain" : undefined,
67
+ maskImage: symbolDataUrl ? `url("${symbolDataUrl}")` : undefined,
68
+ maskPosition: symbolDataUrl ? "center" : undefined,
69
+ maskRepeat: symbolDataUrl ? "no-repeat" : undefined,
70
+ maskSize: symbolDataUrl ? "contain" : undefined,
71
+ }}
72
+ >
73
+ {symbolDataUrl ? null : fallback}
74
+ </span>
75
+ </MacIconButton>
100
76
  }
101
- >
102
- <span
103
- aria-hidden
104
- className="hd-mac-symbol-icon"
105
- style={{
106
- width: glyphPx,
107
- height: glyphPx,
108
- color: "currentColor",
109
- backgroundColor: symbolDataUrl ? "currentColor" : "transparent",
110
- fontFamily: "-apple-system, BlinkMacSystemFont, sans-serif",
111
- fontSize: symbolDataUrl ? "0" : glyphPx,
112
- fontWeight: 500,
113
- lineHeight: "1",
114
- WebkitMaskImage: symbolDataUrl ? `url("${symbolDataUrl}")` : undefined,
115
- WebkitMaskPosition: symbolDataUrl ? "center" : undefined,
116
- WebkitMaskRepeat: symbolDataUrl ? "no-repeat" : undefined,
117
- WebkitMaskSize: symbolDataUrl ? "contain" : undefined,
118
- maskImage: symbolDataUrl ? `url("${symbolDataUrl}")` : undefined,
119
- maskPosition: symbolDataUrl ? "center" : undefined,
120
- maskRepeat: symbolDataUrl ? "no-repeat" : undefined,
121
- maskSize: symbolDataUrl ? "contain" : undefined,
122
- }}
123
- >
124
- {symbolDataUrl ? null : fallback}
125
- </span>
126
- </MacTooltip.Trigger>
77
+ />
127
78
  <MacTooltip.Portal>
128
79
  <MacTooltip.Positioner sideOffset={5}>
129
80
  <MacTooltip.Popup>{label}</MacTooltip.Popup>
@@ -232,7 +232,10 @@
232
232
  .hd-mac-symbol-icon-button {
233
233
  display: inline-grid;
234
234
  place-items: center;
235
+ place-content: center;
235
236
  padding: 0;
237
+ line-height: 0;
238
+ vertical-align: middle;
236
239
  }
237
240
 
238
241
  .hd-mac-icon-button {
@@ -251,11 +254,13 @@
251
254
  }
252
255
 
253
256
  .hd-mac-symbol-icon {
257
+ grid-area: 1 / 1;
254
258
  display: inline-flex;
255
259
  align-items: center;
256
260
  justify-content: center;
257
261
  text-align: center;
258
262
  flex-shrink: 0;
263
+ margin: auto;
259
264
  }
260
265
 
261
266
  .hd-mac-help-button {
@@ -11,6 +11,7 @@ const DEFAULT_POINT_SIZE = 18;
11
11
  const MIN_POINT_SIZE = 10;
12
12
  const MAX_POINT_SIZE = 256;
13
13
  const SYMBOL_SCALE_MULTIPLIER = 2.5;
14
+ const TRANSPARENT_ALPHA_THRESHOLD = 2;
14
15
 
15
16
  export function calculateSystemSymbolPointSize(
16
17
  cssPx: number,
@@ -58,7 +59,7 @@ export async function getSystemSymbolDataUrl(
58
59
  point_size: pointSize,
59
60
  });
60
61
 
61
- const dataUrl = maybeDataUrl ?? null;
62
+ const dataUrl = maybeDataUrl ? await trimTransparentSymbolPadding(maybeDataUrl) : null;
62
63
  SYSTEM_SYMBOL_CACHE.set(key, dataUrl);
63
64
  return dataUrl;
64
65
  } catch (error) {
@@ -66,3 +67,90 @@ export async function getSystemSymbolDataUrl(
66
67
  throw error;
67
68
  }
68
69
  }
70
+
71
+ async function trimTransparentSymbolPadding(dataUrl: string): Promise<string> {
72
+ if (
73
+ typeof window === "undefined" ||
74
+ typeof Image === "undefined" ||
75
+ typeof document === "undefined"
76
+ ) {
77
+ return dataUrl;
78
+ }
79
+
80
+ const image = await loadImage(dataUrl);
81
+ const width = image.naturalWidth || image.width;
82
+ const height = image.naturalHeight || image.height;
83
+ if (width <= 0 || height <= 0) {
84
+ return dataUrl;
85
+ }
86
+
87
+ const canvas = document.createElement("canvas");
88
+ canvas.width = width;
89
+ canvas.height = height;
90
+
91
+ const context = canvas.getContext("2d", { willReadFrequently: true });
92
+ if (!context) {
93
+ return dataUrl;
94
+ }
95
+
96
+ context.drawImage(image, 0, 0);
97
+ const pixels = context.getImageData(0, 0, width, height).data;
98
+ let minX = width;
99
+ let minY = height;
100
+ let maxX = -1;
101
+ let maxY = -1;
102
+
103
+ for (let y = 0; y < height; y += 1) {
104
+ for (let x = 0; x < width; x += 1) {
105
+ const alpha = pixels[(y * width + x) * 4 + 3];
106
+ if (alpha > TRANSPARENT_ALPHA_THRESHOLD) {
107
+ minX = Math.min(minX, x);
108
+ minY = Math.min(minY, y);
109
+ maxX = Math.max(maxX, x);
110
+ maxY = Math.max(maxY, y);
111
+ }
112
+ }
113
+ }
114
+
115
+ if (maxX < minX || maxY < minY) {
116
+ return dataUrl;
117
+ }
118
+
119
+ const trimmedWidth = maxX - minX + 1;
120
+ const trimmedHeight = maxY - minY + 1;
121
+ if (trimmedWidth === width && trimmedHeight === height) {
122
+ return dataUrl;
123
+ }
124
+
125
+ const trimmedCanvas = document.createElement("canvas");
126
+ trimmedCanvas.width = trimmedWidth;
127
+ trimmedCanvas.height = trimmedHeight;
128
+
129
+ const trimmedContext = trimmedCanvas.getContext("2d");
130
+ if (!trimmedContext) {
131
+ return dataUrl;
132
+ }
133
+
134
+ trimmedContext.drawImage(
135
+ canvas,
136
+ minX,
137
+ minY,
138
+ trimmedWidth,
139
+ trimmedHeight,
140
+ 0,
141
+ 0,
142
+ trimmedWidth,
143
+ trimmedHeight,
144
+ );
145
+
146
+ return trimmedCanvas.toDataURL("image/png");
147
+ }
148
+
149
+ function loadImage(dataUrl: string): Promise<HTMLImageElement> {
150
+ return new Promise((resolve, reject) => {
151
+ const image = new Image();
152
+ image.onload = () => resolve(image);
153
+ image.onerror = () => reject(new Error("Failed to load SF Symbol PNG data."));
154
+ image.src = dataUrl;
155
+ });
156
+ }