@tutti-os/ui-system 0.0.227 → 0.0.229

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.
@@ -23,6 +23,8 @@ import {
23
23
  // src/components/avatar/avatar.tsx
24
24
  import { Avatar as AvatarPrimitive } from "radix-ui";
25
25
  import {
26
+ useEffect,
27
+ useRef,
26
28
  useState
27
29
  } from "react";
28
30
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -32,15 +34,52 @@ var sizeClassNames = {
32
34
  md: "size-8",
33
35
  lg: "size-10"
34
36
  };
37
+ var sizePixels = {
38
+ xs: 16,
39
+ sm: 24,
40
+ md: 32,
41
+ lg: 40
42
+ };
43
+ var avatarDeliveryBuckets = [32, 48, 64, 96, 128, 192, 256, 384, 512];
35
44
  function avatarInitial(label, initial) {
36
45
  const value = initial?.trim() || label.trim();
37
46
  return Array.from(value)[0]?.toLocaleUpperCase() || "?";
38
47
  }
48
+ function avatarSizePixels(size) {
49
+ return typeof size === "number" && Number.isFinite(size) && size > 0 ? size : typeof size === "number" ? sizePixels.md : sizePixels[size];
50
+ }
51
+ function avatarDeliveryDimension(value) {
52
+ const desiredSize = Math.ceil(value * 2);
53
+ return avatarDeliveryBuckets.find((bucket) => bucket >= desiredSize) ?? avatarDeliveryBuckets.at(-1) ?? desiredSize;
54
+ }
55
+ function avatarTransformUrl(src, dimensions) {
56
+ let url;
57
+ try {
58
+ url = new URL(src);
59
+ } catch {
60
+ return src;
61
+ }
62
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
63
+ return src;
64
+ }
65
+ const fragmentIndex = src.indexOf("#");
66
+ const sourceWithoutFragment = fragmentIndex >= 0 ? src.slice(0, fragmentIndex) : src;
67
+ const fragment = fragmentIndex >= 0 ? src.slice(fragmentIndex) : "";
68
+ const separator = sourceWithoutFragment.includes("?") ? sourceWithoutFragment.endsWith("?") || sourceWithoutFragment.endsWith("&") ? "" : "&" : "?";
69
+ const transformQuery = new URLSearchParams({
70
+ width: String(avatarDeliveryDimension(dimensions.width)),
71
+ height: String(avatarDeliveryDimension(dimensions.height)),
72
+ format: "webp",
73
+ fit: "inside"
74
+ });
75
+ return `${sourceWithoutFragment}${separator}${transformQuery}${fragment}`;
76
+ }
39
77
  function Avatar({
40
78
  children,
41
79
  className,
42
80
  fallback = "initial",
43
81
  fallbackColor,
82
+ fallbackSrc,
44
83
  imageClassName,
45
84
  imageProps,
46
85
  initial,
@@ -50,10 +89,24 @@ function Avatar({
50
89
  src,
51
90
  style,
52
91
  surfaceClassName,
92
+ transform = false,
53
93
  ...rootProps
54
94
  }) {
55
95
  const normalizedSrc = src?.trim() ?? "";
56
- const effectiveImageSrc = loading ? "" : normalizedSrc;
96
+ const normalizedFallbackSrc = fallbackSrc?.trim() ?? "";
97
+ const initialSize = avatarSizePixels(size);
98
+ const surfaceRef = useRef(null);
99
+ const [renderedDimensions, setRenderedDimensions] = useState(null);
100
+ const [failedTransformUrl, setFailedTransformUrl] = useState("");
101
+ const [failedOriginalUrl, setFailedOriginalUrl] = useState("");
102
+ const transformedImageSrc = transform ? avatarTransformUrl(
103
+ normalizedSrc,
104
+ renderedDimensions ?? { height: initialSize, width: initialSize }
105
+ ) : normalizedSrc;
106
+ const shouldRetryOriginal = transformedImageSrc !== normalizedSrc && failedTransformUrl === transformedImageSrc;
107
+ const originalCandidateSrc = shouldRetryOriginal ? normalizedSrc : transformedImageSrc;
108
+ const shouldUseFallback = normalizedSrc.length > 0 && originalCandidateSrc === normalizedSrc && failedOriginalUrl === normalizedSrc;
109
+ const effectiveImageSrc = loading ? "" : shouldUseFallback ? normalizedFallbackSrc : originalCandidateSrc;
57
110
  const [imageState, setImageState] = useState({ src: "", status: "idle" });
58
111
  const imageStatus = imageState.src === effectiveImageSrc ? imageState.status : effectiveImageSrc ? "loading" : "error";
59
112
  const showImage = imageStatus === "loaded" && effectiveImageSrc.length > 0;
@@ -61,6 +114,46 @@ function Avatar({
61
114
  const imageSrcForPrimitive = imageStatus === "error" ? void 0 : effectiveImageSrc || void 0;
62
115
  const avatarState = loading ? "loading" : effectiveImageSrc.length === 0 ? fallback : imageStatus === "loading" || imageStatus === "idle" ? "loading" : showImage ? "image" : fallback;
63
116
  const numericSizeStyle = typeof size === "number" ? { height: `${size}px`, width: `${size}px`, ...style } : style;
117
+ useEffect(() => {
118
+ setFailedTransformUrl("");
119
+ setFailedOriginalUrl("");
120
+ }, [normalizedFallbackSrc, normalizedSrc, transform]);
121
+ useEffect(() => {
122
+ const surface = surfaceRef.current;
123
+ if (!surface) {
124
+ return;
125
+ }
126
+ const updateDimensions = (width, height) => {
127
+ if (width <= 0 || height <= 0) {
128
+ return;
129
+ }
130
+ setRenderedDimensions(
131
+ (current) => current?.width === width && current.height === height ? current : { height, width }
132
+ );
133
+ };
134
+ const rect = surface.getBoundingClientRect();
135
+ updateDimensions(rect.width, rect.height);
136
+ if (typeof ResizeObserver === "undefined") {
137
+ return;
138
+ }
139
+ const observer = new ResizeObserver(([entry]) => {
140
+ if (entry) {
141
+ updateDimensions(entry.contentRect.width, entry.contentRect.height);
142
+ }
143
+ });
144
+ observer.observe(surface);
145
+ return () => {
146
+ observer.disconnect();
147
+ };
148
+ }, []);
149
+ const markImageError = () => {
150
+ if (transformedImageSrc !== normalizedSrc && effectiveImageSrc === transformedImageSrc && shouldRetryOriginal === false) {
151
+ setFailedTransformUrl(transformedImageSrc);
152
+ } else if (effectiveImageSrc === normalizedSrc) {
153
+ setFailedOriginalUrl(normalizedSrc);
154
+ }
155
+ setImageState({ src: effectiveImageSrc, status: "error" });
156
+ };
64
157
  return /* @__PURE__ */ jsxs(
65
158
  AvatarPrimitive.Root,
66
159
  {
@@ -84,6 +177,7 @@ function Avatar({
84
177
  surfaceClassName
85
178
  ),
86
179
  "data-slot": "avatar-surface",
180
+ ref: surfaceRef,
87
181
  style: fallback === "initial" && fallbackColor && !showImage && !loading && !imagePending ? { backgroundColor: fallbackColor } : void 0,
88
182
  children: [
89
183
  /* @__PURE__ */ jsx(
@@ -97,11 +191,15 @@ function Avatar({
97
191
  ),
98
192
  src: imageSrcForPrimitive,
99
193
  onError: (event) => {
100
- setImageState({ src: effectiveImageSrc, status: "error" });
194
+ markImageError();
101
195
  imageProps?.onError?.(event);
102
196
  },
103
197
  onLoadingStatusChange: (status) => {
104
- setImageState({ src: effectiveImageSrc, status });
198
+ if (status === "error") {
199
+ markImageError();
200
+ } else {
201
+ setImageState({ src: effectiveImageSrc, status });
202
+ }
105
203
  imageProps?.onLoadingStatusChange?.(status);
106
204
  }
107
205
  }
@@ -3125,7 +3223,7 @@ function SectionTabs({
3125
3223
  }
3126
3224
 
3127
3225
  // src/components/segment-bar/segment-bar.tsx
3128
- import { useEffect as useEffect5, useLayoutEffect as useLayoutEffect2, useRef as useRef4, useState as useState8 } from "react";
3226
+ import { useEffect as useEffect6, useLayoutEffect as useLayoutEffect2, useRef as useRef5, useState as useState8 } from "react";
3129
3227
  import { jsx as jsx26, jsxs as jsxs15 } from "react/jsx-runtime";
3130
3228
  function SegmentBar({
3131
3229
  segments,
@@ -3135,8 +3233,8 @@ function SegmentBar({
3135
3233
  className,
3136
3234
  testId
3137
3235
  }) {
3138
- const containerRef = useRef4(null);
3139
- const buttonRefs = useRef4({});
3236
+ const containerRef = useRef5(null);
3237
+ const buttonRefs = useRef5({});
3140
3238
  const [indicator, setIndicator] = useState8({
3141
3239
  left: 0,
3142
3240
  width: 0,
@@ -3159,7 +3257,7 @@ function SegmentBar({
3159
3257
  (current) => current.left === next.left && current.width === next.width && current.ready === next.ready ? current : next
3160
3258
  );
3161
3259
  }, [segments, value]);
3162
- useEffect5(() => {
3260
+ useEffect6(() => {
3163
3261
  const container = containerRef.current;
3164
3262
  if (!container || typeof ResizeObserver === "undefined") {
3165
3263
  return;
@@ -4110,7 +4208,7 @@ function ToastViewport({
4110
4208
  }
4111
4209
 
4112
4210
  // src/components/underline-tabs/underline-tabs.tsx
4113
- import { useEffect as useEffect7, useLayoutEffect as useLayoutEffect4, useRef as useRef5, useState as useState10 } from "react";
4211
+ import { useEffect as useEffect8, useLayoutEffect as useLayoutEffect4, useRef as useRef6, useState as useState10 } from "react";
4114
4212
  import { jsx as jsx36, jsxs as jsxs18 } from "react/jsx-runtime";
4115
4213
  function UnderlineTabs({
4116
4214
  tabs,
@@ -4126,9 +4224,9 @@ function UnderlineTabs({
4126
4224
  scrollRightTestId,
4127
4225
  preventMouseDownDefault = false
4128
4226
  }) {
4129
- const viewportRef = useRef5(null);
4130
- const rowRef = useRef5(null);
4131
- const buttonRefs = useRef5({});
4227
+ const viewportRef = useRef6(null);
4228
+ const rowRef = useRef6(null);
4229
+ const buttonRefs = useRef6({});
4132
4230
  const [indicatorStyle, setIndicatorStyle] = useState10({ left: 0, width: 0 });
4133
4231
  const [overflow, setOverflow] = useState10({
4134
4232
  canScrollLeft: false,
@@ -4151,7 +4249,7 @@ function UnderlineTabs({
4151
4249
  (current) => current.left === nextStyle.left && current.width === nextStyle.width ? current : nextStyle
4152
4250
  );
4153
4251
  }, [tabs, value]);
4154
- useEffect7(() => {
4252
+ useEffect8(() => {
4155
4253
  const viewport = viewportRef.current;
4156
4254
  if (!viewport) {
4157
4255
  return;
@@ -4743,4 +4841,4 @@ export {
4743
4841
  UnderlineTabs,
4744
4842
  ViewportMenuSurface
4745
4843
  };
4746
- //# sourceMappingURL=chunk-FQWIOPDU.js.map
4844
+ //# sourceMappingURL=chunk-GYBECSSN.js.map