@boyernick/standard-ui-react 0.4.2-canary.52 → 0.4.2-canary.54

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boyernick/standard-ui-react",
3
- "version": "0.4.2-canary.52",
3
+ "version": "0.4.2-canary.54",
4
4
  "description": "React components for StandardUI",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/icons.tsx CHANGED
@@ -1,6 +1,9 @@
1
1
  "use client"
2
2
 
3
- import type { CentralIconBaseProps } from "@central-icons-react/round-outlined-radius-2-stroke-2/CentralIconBase"
3
+ import {
4
+ CentralIconBase,
5
+ type CentralIconBaseProps,
6
+ } from "@central-icons-react/round-outlined-radius-2-stroke-2/CentralIconBase"
4
7
  import { IconArrowDown as IconArrowDownBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconArrowDown"
5
8
  import { IconBell as IconBellBase } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconBell"
6
9
  import { IconCalendar1 as IconCalendar1Base } from "@central-icons-react/round-outlined-radius-2-stroke-2/IconCalendar1"
@@ -63,6 +66,32 @@ export const withCentralIconDefaults = (
63
66
  }
64
67
 
65
68
  export const IconArrowDown = withCentralIconDefaults(IconArrowDownBase)
69
+
70
+ /**
71
+ * Central ships a Small cut of the cross and the chevrons but not of the down
72
+ * arrow, whose full-size glyph is 16 units tall and outweighs IconCrossSmall
73
+ * beside it. Drawn on Central's base with the same 2px round stroke, 9 × 12
74
+ * units: a touch larger than the cross's 8 × 8 so it holds its own.
75
+ */
76
+ const IconArrowDownSmallBase = (props: CentralIconBaseProps) => (
77
+ <CentralIconBase
78
+ {...props}
79
+ ariaLabel="arrow-down-small, download"
80
+ maskId="round-outlined-radius-2-stroke-2-IconArrowDownSmall"
81
+ >
82
+ <path
83
+ d="M12 6V18M7.5 13.5L12 18L16.5 13.5"
84
+ stroke="currentColor"
85
+ strokeWidth="2"
86
+ strokeLinecap="round"
87
+ strokeLinejoin="round"
88
+ />
89
+ </CentralIconBase>
90
+ )
91
+
92
+ export const IconArrowDownSmall = withCentralIconDefaults(
93
+ IconArrowDownSmallBase,
94
+ )
66
95
  export const IconBell = withCentralIconDefaults(IconBellBase)
67
96
  export const IconCalendar1 = withCentralIconDefaults(IconCalendar1Base)
68
97
  export const IconChainLink1 = withCentralIconDefaults(IconChainLink1Base)
@@ -124,6 +153,7 @@ export const iconGallery = [
124
153
  { name: "IconPlus", Icon: IconPlus },
125
154
  { name: "IconMinus", Icon: IconMinus },
126
155
  { name: "IconCrossSmall", Icon: IconCrossSmall },
156
+ { name: "IconArrowDownSmall", Icon: IconArrowDownSmall },
127
157
  { name: "IconX", Icon: IconX },
128
158
  { name: "IconCheckmark1", Icon: IconCheckmark1 },
129
159
  { name: "IconChevronBottom", Icon: IconChevronBottom },
package/src/index.ts CHANGED
@@ -967,6 +967,7 @@ export {
967
967
  withCentralIconDefaults,
968
968
  iconGallery,
969
969
  IconArrowDown,
970
+ IconArrowDownSmall,
970
971
  IconBell,
971
972
  IconCalendar1,
972
973
  IconChainLink1,
package/src/modal.tsx CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  DialogTrigger,
32
32
  } from "./dialog"
33
33
  import {
34
- IconArrowDown,
34
+ IconArrowDownSmall,
35
35
  IconChevronRightSmall,
36
36
  IconCrossSmall,
37
37
  } from "./icons"
@@ -174,15 +174,39 @@ const ModalImage = ({
174
174
  }: Pick<GalleryItem, "src" | "alt" | "imgProps"> & {
175
175
  fit?: "viewport" | "contain"
176
176
  }) => {
177
- const { className, ...props } = imgProps ?? {}
177
+ const { className, onLoad, onError, ...props } = imgProps ?? {}
178
+ // Held transparent until decoded, then faded in: a large file otherwise
179
+ // paints in top-down bands inside the lightbox. The ref catches an image the
180
+ // browser already has (a cached file, or the one the trigger just showed)
181
+ // before first paint, so it appears at once with nothing to fade.
182
+ const [loaded, setLoaded] = useState(false)
183
+ const catchLoaded = useCallback((image: HTMLImageElement | null) => {
184
+ if (image?.complete && image.naturalWidth > 0) setLoaded(true)
185
+ }, [])
178
186
 
179
187
  return (
180
188
  // eslint-disable-next-line @next/next/no-img-element
181
189
  <img
190
+ ref={catchLoaded}
182
191
  src={src}
183
192
  alt={alt}
184
193
  draggable={false}
194
+ onLoad={(event) => {
195
+ onLoad?.(event)
196
+ event.currentTarget
197
+ .decode()
198
+ .catch(() => {})
199
+ .then(() => setLoaded(true))
200
+ }}
201
+ onError={(event) => {
202
+ onError?.(event)
203
+ // Reveal the broken image rather than leave an invisible one.
204
+ setLoaded(true)
205
+ }}
185
206
  className={cn(
207
+ // Linear, like every fade in `motion` — see the note there.
208
+ "transition-opacity duration-[var(--duration-lg)] ease-linear motion-reduce:transition-none",
209
+ !loaded && "opacity-0",
186
210
  "rounded-lg object-contain shadow-lg select-none",
187
211
  fit === "contain"
188
212
  ? "max-h-full max-w-full"
@@ -233,10 +257,12 @@ const ModalDownload = ({
233
257
  iconOnly: true,
234
258
  }),
235
259
  modalControlClassName,
236
- "absolute top-4 right-4 z-20",
260
+ // Download sits top-left and Close top-right, where dismissal is
261
+ // expected; the gallery's previous/next arrows stay vertically centred.
262
+ "absolute top-4 left-4 z-20",
237
263
  )}
238
264
  >
239
- <IconArrowDown aria-hidden />
265
+ <IconArrowDownSmall aria-hidden />
240
266
  </a>
241
267
  </ModalControlTooltip>
242
268
  )
@@ -254,7 +280,7 @@ const ModalDismiss = () => (
254
280
  aria-label="Close image"
255
281
  className={cn(
256
282
  modalControlClassName,
257
- "absolute top-4 left-4 z-20",
283
+ "absolute top-4 right-4 z-20",
258
284
  )}
259
285
  />
260
286
  }