@jobber/components 8.8.0 → 8.9.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.
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { LightBoxNavigationProps, LightBoxProps } from "./LightBox.types";
2
+ import type { LightBoxNavigationProps, LightBoxProps, LightBoxThumbnailBarProps, LightBoxThumbnailImageProps, LightBoxThumbnailProps } from "./LightBox.types";
3
3
  import { LightBoxProvider } from "./LightBoxContext";
4
4
  export declare function LightBoxContent(): React.JSX.Element;
5
5
  /**
@@ -75,7 +75,25 @@ declare function LightBoxCaption(): React.JSX.Element | null;
75
75
  /**
76
76
  * Scrollable thumbnail strip. Only renders when there are two or more images.
77
77
  */
78
- declare function LightBoxThumbnails(): React.JSX.Element | null;
78
+ declare function LightBoxThumbnails(): React.JSX.Element;
79
+ /**
80
+ * Scrollable container for the thumbnail strip. Owns the scroll chrome and
81
+ * box-sizing, and only renders when there are two or more images.
82
+ *
83
+ * Pass one `LightBox.Thumbnail` per image as `children` to own the thumbnail
84
+ * loop.
85
+ */
86
+ declare function LightBoxThumbnailBar({ children, className, }: LightBoxThumbnailBarProps): React.JSX.Element | null;
87
+ /**
88
+ * A selectable thumbnail cell identified by its `index`. Owns the selected
89
+ * styling, click-to-navigate behaviour, and renders consumer-provided
90
+ * `children` (typically a `LightBox.ThumbnailImage`).
91
+ */
92
+ declare function LightBoxThumbnail({ index, children, className, }: LightBoxThumbnailProps): React.JSX.Element;
93
+ /**
94
+ * A thumbnail using our default styling.
95
+ */
96
+ declare function LightBoxThumbnailImage({ src, alt, className, }: LightBoxThumbnailImageProps): React.JSX.Element;
79
97
  /**
80
98
  * LightBox displays images in a fullscreen overlay.
81
99
  *
@@ -128,5 +146,8 @@ declare namespace LightBox {
128
146
  var Navigation: typeof LightBoxNavigation;
129
147
  var Caption: typeof LightBoxCaption;
130
148
  var Thumbnails: typeof LightBoxThumbnails;
149
+ var ThumbnailBar: typeof LightBoxThumbnailBar;
150
+ var Thumbnail: typeof LightBoxThumbnail;
151
+ var ThumbnailImage: typeof LightBoxThumbnailImage;
131
152
  }
132
153
  export { LightBox };
@@ -92,3 +92,44 @@ export interface LightBoxNavigationProps {
92
92
  */
93
93
  readonly nextButtonClassName?: string;
94
94
  }
95
+ export interface LightBoxThumbnailBarProps {
96
+ /**
97
+ * The thumbnails to render inside the scrollable bar, typically one
98
+ * `LightBox.Thumbnail` per image.
99
+ */
100
+ readonly children: ReactNode;
101
+ /**
102
+ * Additional class name to apply to the thumbnail bar wrapper.
103
+ */
104
+ readonly className?: string;
105
+ }
106
+ export interface LightBoxThumbnailProps {
107
+ /**
108
+ * The index of the image this thumbnail represents. Used to derive the
109
+ * selected state and to navigate to the image when activated.
110
+ */
111
+ readonly index: number;
112
+ /**
113
+ * The content rendered inside the selectable thumbnail cell, typically a
114
+ * `LightBox.ThumbnailImage`.
115
+ */
116
+ readonly children: ReactNode;
117
+ /**
118
+ * Additional class name to apply to the thumbnail cell.
119
+ */
120
+ readonly className?: string;
121
+ }
122
+ export interface LightBoxThumbnailImageProps {
123
+ /**
124
+ * The image source to render.
125
+ */
126
+ readonly src: string;
127
+ /**
128
+ * The alternative text for the image.
129
+ */
130
+ readonly alt: string;
131
+ /**
132
+ * Additional class name to apply to the image.
133
+ */
134
+ readonly className?: string;
135
+ }
@@ -305,13 +305,40 @@ function LightBoxCaption() {
305
305
  * Scrollable thumbnail strip. Only renders when there are two or more images.
306
306
  */
307
307
  function LightBoxThumbnails() {
308
- const { images, currentImageIndex, boxSizing, selectedThumbnailRef, handleThumbnailClick, } = useLightBoxContext();
308
+ const { images } = useLightBoxContext();
309
+ return (React.createElement(LightBoxThumbnailBar, null, images.map((image, index) => (React.createElement(LightBoxThumbnail, { key: index, index: index },
310
+ React.createElement(LightBoxThumbnailImage, { src: image.url, alt: image.alt || image.title || "" }))))));
311
+ }
312
+ /**
313
+ * Scrollable container for the thumbnail strip. Owns the scroll chrome and
314
+ * box-sizing, and only renders when there are two or more images.
315
+ *
316
+ * Pass one `LightBox.Thumbnail` per image as `children` to own the thumbnail
317
+ * loop.
318
+ */
319
+ function LightBoxThumbnailBar({ children, className, }) {
320
+ const { images, boxSizing } = useLightBoxContext();
309
321
  if (images.length <= 1)
310
322
  return null;
311
- return (React.createElement("div", { className: styles.thumbnailBar, style: { "--lightbox--box-sizing": boxSizing }, "data-testid": "ATL-Thumbnail-Bar" }, images.map((image, index) => (React.createElement("div", { key: index, className: classnames(styles.thumbnail, {
312
- [styles.selected]: index === currentImageIndex,
313
- }), onClick: () => handleThumbnailClick(index), ref: index === currentImageIndex ? selectedThumbnailRef : null },
314
- React.createElement("img", { key: index, src: image.url, alt: image.alt || image.title || "", className: styles.thumbnailImage }))))));
323
+ return (React.createElement("div", { className: classnames(styles.thumbnailBar, className), style: { "--lightbox--box-sizing": boxSizing }, "data-testid": "ATL-Thumbnail-Bar" }, children));
324
+ }
325
+ /**
326
+ * A selectable thumbnail cell identified by its `index`. Owns the selected
327
+ * styling, click-to-navigate behaviour, and renders consumer-provided
328
+ * `children` (typically a `LightBox.ThumbnailImage`).
329
+ */
330
+ function LightBoxThumbnail({ index, children, className, }) {
331
+ const { currentImageIndex, selectedThumbnailRef, handleThumbnailClick } = useLightBoxContext();
332
+ const selected = index === currentImageIndex;
333
+ return (React.createElement("div", { className: classnames(styles.thumbnail, className, {
334
+ [styles.selected]: selected,
335
+ }), onClick: () => handleThumbnailClick(index), ref: selected ? selectedThumbnailRef : null }, children));
336
+ }
337
+ /**
338
+ * A thumbnail using our default styling.
339
+ */
340
+ function LightBoxThumbnailImage({ src, alt, className, }) {
341
+ return (React.createElement("img", { src: src, alt: alt, className: classnames(styles.thumbnailImage, className) }));
315
342
  }
316
343
  /**
317
344
  * LightBox displays images in a fullscreen overlay.
@@ -367,6 +394,9 @@ LightBox.Slides = LightBoxSlides;
367
394
  LightBox.Navigation = LightBoxNavigation;
368
395
  LightBox.Caption = LightBoxCaption;
369
396
  LightBox.Thumbnails = LightBoxThumbnails;
397
+ LightBox.ThumbnailBar = LightBoxThumbnailBar;
398
+ LightBox.Thumbnail = LightBoxThumbnail;
399
+ LightBox.ThumbnailImage = LightBoxThumbnailImage;
370
400
 
371
401
  exports.LightBox = LightBox;
372
402
  exports.useLightBoxContext = useLightBoxContext;
@@ -303,13 +303,40 @@ function LightBoxCaption() {
303
303
  * Scrollable thumbnail strip. Only renders when there are two or more images.
304
304
  */
305
305
  function LightBoxThumbnails() {
306
- const { images, currentImageIndex, boxSizing, selectedThumbnailRef, handleThumbnailClick, } = useLightBoxContext();
306
+ const { images } = useLightBoxContext();
307
+ return (React__default.createElement(LightBoxThumbnailBar, null, images.map((image, index) => (React__default.createElement(LightBoxThumbnail, { key: index, index: index },
308
+ React__default.createElement(LightBoxThumbnailImage, { src: image.url, alt: image.alt || image.title || "" }))))));
309
+ }
310
+ /**
311
+ * Scrollable container for the thumbnail strip. Owns the scroll chrome and
312
+ * box-sizing, and only renders when there are two or more images.
313
+ *
314
+ * Pass one `LightBox.Thumbnail` per image as `children` to own the thumbnail
315
+ * loop.
316
+ */
317
+ function LightBoxThumbnailBar({ children, className, }) {
318
+ const { images, boxSizing } = useLightBoxContext();
307
319
  if (images.length <= 1)
308
320
  return null;
309
- return (React__default.createElement("div", { className: styles.thumbnailBar, style: { "--lightbox--box-sizing": boxSizing }, "data-testid": "ATL-Thumbnail-Bar" }, images.map((image, index) => (React__default.createElement("div", { key: index, className: classnames(styles.thumbnail, {
310
- [styles.selected]: index === currentImageIndex,
311
- }), onClick: () => handleThumbnailClick(index), ref: index === currentImageIndex ? selectedThumbnailRef : null },
312
- React__default.createElement("img", { key: index, src: image.url, alt: image.alt || image.title || "", className: styles.thumbnailImage }))))));
321
+ return (React__default.createElement("div", { className: classnames(styles.thumbnailBar, className), style: { "--lightbox--box-sizing": boxSizing }, "data-testid": "ATL-Thumbnail-Bar" }, children));
322
+ }
323
+ /**
324
+ * A selectable thumbnail cell identified by its `index`. Owns the selected
325
+ * styling, click-to-navigate behaviour, and renders consumer-provided
326
+ * `children` (typically a `LightBox.ThumbnailImage`).
327
+ */
328
+ function LightBoxThumbnail({ index, children, className, }) {
329
+ const { currentImageIndex, selectedThumbnailRef, handleThumbnailClick } = useLightBoxContext();
330
+ const selected = index === currentImageIndex;
331
+ return (React__default.createElement("div", { className: classnames(styles.thumbnail, className, {
332
+ [styles.selected]: selected,
333
+ }), onClick: () => handleThumbnailClick(index), ref: selected ? selectedThumbnailRef : null }, children));
334
+ }
335
+ /**
336
+ * A thumbnail using our default styling.
337
+ */
338
+ function LightBoxThumbnailImage({ src, alt, className, }) {
339
+ return (React__default.createElement("img", { src: src, alt: alt, className: classnames(styles.thumbnailImage, className) }));
313
340
  }
314
341
  /**
315
342
  * LightBox displays images in a fullscreen overlay.
@@ -365,5 +392,8 @@ LightBox.Slides = LightBoxSlides;
365
392
  LightBox.Navigation = LightBoxNavigation;
366
393
  LightBox.Caption = LightBoxCaption;
367
394
  LightBox.Thumbnails = LightBoxThumbnails;
395
+ LightBox.ThumbnailBar = LightBoxThumbnailBar;
396
+ LightBox.Thumbnail = LightBoxThumbnail;
397
+ LightBox.ThumbnailImage = LightBoxThumbnailImage;
368
398
 
369
399
  export { LightBox as L, useLightBoxContext as u };
@@ -43,6 +43,10 @@ LightBox exposes its internal building blocks as subcomponents:
43
43
  `LightBox.Navigation`, `LightBox.Caption`, and `LightBox.Thumbnails`. This gives
44
44
  you more control over the LightBox's appearance and behaviour.
45
45
 
46
+ The thumbnail strip is itself composed of smaller subcomponents you can use
47
+ directly: `LightBox.ThumbnailBar`, `LightBox.Thumbnail`, and
48
+ `LightBox.ThumbnailImage`. See [Custom thumbnails](#custom-thumbnails) below.
49
+
46
50
  Here's a basic example of how the non-composable LightBox is used:
47
51
 
48
52
  ```tsx
@@ -157,6 +161,33 @@ function CustomControls() {
157
161
  }
158
162
  ```
159
163
 
164
+ ### Custom thumbnails
165
+
166
+ `LightBox.Thumbnails` renders the default thumbnail strip and requires no
167
+ configuration. When you need to control how each thumbnail renders, compose the
168
+ thumbnail subcomponents yourself:
169
+
170
+ ```tsx
171
+ import { LightBox, useLightBoxContext } from "@jobber/components";
172
+
173
+ function CustomThumbnails() {
174
+ const { images } = useLightBoxContext();
175
+
176
+ return (
177
+ <LightBox.ThumbnailBar>
178
+ {images.map((image, index) => (
179
+ <LightBox.Thumbnail key={index} index={index}>
180
+ <LightBox.ThumbnailImage
181
+ src={image.url}
182
+ alt={image.alt ?? image.title ?? ""}
183
+ />
184
+ </LightBox.Thumbnail>
185
+ ))}
186
+ </LightBox.ThumbnailBar>
187
+ );
188
+ }
189
+ ```
190
+
160
191
  ## Testing
161
192
 
162
193
  When using Jest to test LightBox implementations, you will need to include this
@@ -223,3 +254,26 @@ window.HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
223
254
  | Prop | Type | Required | Default | Description |
224
255
  |------|------|----------|---------|-------------|
225
256
  | `className` | `string` | No | — | |
257
+
258
+ #### LightBox.Thumbnail
259
+
260
+ | Prop | Type | Required | Default | Description |
261
+ |------|------|----------|---------|-------------|
262
+ | `children` | `ReactNode` | Yes | — | The content rendered inside the selectable thumbnail cell, typically a `LightBox.ThumbnailImage`. |
263
+ | `index` | `number` | Yes | — | The index of the image this thumbnail represents. Used to derive the selected state and to navigate to the image when... |
264
+ | `className` | `string` | No | — | Additional class name to apply to the thumbnail cell. |
265
+
266
+ #### LightBox.ThumbnailBar
267
+
268
+ | Prop | Type | Required | Default | Description |
269
+ |------|------|----------|---------|-------------|
270
+ | `children` | `ReactNode` | Yes | — | The thumbnails to render inside the scrollable bar, typically one `LightBox.Thumbnail` per image. |
271
+ | `className` | `string` | No | — | Additional class name to apply to the thumbnail bar wrapper. |
272
+
273
+ #### LightBox.ThumbnailImage
274
+
275
+ | Prop | Type | Required | Default | Description |
276
+ |------|------|----------|---------|-------------|
277
+ | `alt` | `string` | Yes | — | The alternative text for the image. |
278
+ | `src` | `string` | Yes | — | The image source to render. |
279
+ | `className` | `string` | No | — | Additional class name to apply to the image. |
package/dist/styles.css CHANGED
@@ -9,6 +9,14 @@
9
9
  * 3. The active <circle>'s stroke-dasharray expands and contracts so the
10
10
  * visible arc length grows and shrinks (Layer 3); stroke-dashoffset
11
11
  * slides the arc around the ring across the cycle.
12
+ *
13
+ * Durations are Material's spec scaled by 1.15× for a calmer pace
14
+ * (Material: 1568 / 5332 / 1333). The cycle:arc ratio is held at exactly
15
+ * 4:1 (6132 = 4 × 1533) so the four blooms per rotation stay evenly spaced.
16
+ *
17
+ * The `small` variant uses only Layer 1 — a fixed-length arc spun at a
18
+ * constant speed (~1.8× faster than base) — because the layered
19
+ * expand/rotate motion reads as too busy at the smaller size.
12
20
  */
13
21
 
14
22
  .YST686vLR3c- {
@@ -43,24 +51,14 @@
43
51
  height: var(--space-base);
44
52
  }
45
53
 
46
- /*
47
- * Layer 1 — continuous linear rotation on the SVG.
48
- *
49
- * The three indicator durations (1568ms outer rotation, 5332ms inner
50
- * 8-phase cycle, 1333ms arc expand/contract) are taken from Material
51
- * Web's published progress-motion spec. They are tuned against each
52
- * other to produce the canonical Material rhythm and are intentionally
53
- * inlined as literals because no other component currently consumes
54
- * them; promote them to design tokens once a second consumer (e.g. a
55
- * future `ProgressIndicator`) needs the same values.
56
- */
54
+ /* Layer 1 — continuous linear rotation on the SVG. */
57
55
 
58
56
  .Av9z-aLocss- {
59
57
  display: block;
60
58
  width: 100%;
61
59
  height: 100%;
62
- -webkit-animation: WvHFS--Nm80- 1568ms linear infinite;
63
- animation: WvHFS--Nm80- 1568ms linear infinite;
60
+ -webkit-animation: WvHFS--Nm80- 1803ms linear infinite;
61
+ animation: WvHFS--Nm80- 1803ms linear infinite;
64
62
  will-change: transform;
65
63
  }
66
64
 
@@ -78,14 +76,12 @@
78
76
  }
79
77
 
80
78
  /*
81
- * Layer 2 + 3 — the active arc.
79
+ * Layer 2 + 3 — the active arc. The circle has circumference 2π·20 ≈ 125.66
80
+ * (SVG units); the "200" gap exceeds it so only one segment shows at a time.
81
+ * stroke-linecap: round gives the soft pill-shaped tips.
82
82
  *
83
- * The circle has circumference 2π·20 125.66 (SVG units). The dasharray
84
- * (visible, gap) interpolates so the visible portion grows from ~1 unit to
85
- * ~95 units, while the dashoffset slides the arc around the ring. The
86
- * combined cycle is the canonical Material rhythm.
87
- *
88
- * stroke-linecap: round produces the soft pill-shaped tips on the arc.
83
+ * The expand animation is delayed by half its period so it starts fully
84
+ * bloomed and its phase is offset from the rotation start.
89
85
  */
90
86
 
91
87
  ._9XO0290QNCY- {
@@ -96,16 +92,35 @@
96
92
  stroke-linecap: round;
97
93
  -webkit-transform-origin: center;
98
94
  transform-origin: center;
99
- -webkit-animation:
100
- SHHICknKvqA- 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,
101
- _0m0Nzc5awnA- 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;
102
- animation:
103
- SHHICknKvqA- 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,
104
- _0m0Nzc5awnA- 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;
95
+ -webkit-animation: SHHICknKvqA- 6132ms cubic-bezier(0.4, 0, 0.2, 1) infinite
96
+ both,
97
+ _0m0Nzc5awnA- 1533ms linear infinite both;
98
+ animation: SHHICknKvqA- 6132ms cubic-bezier(0.4, 0, 0.2, 1) infinite
99
+ both,
100
+ _0m0Nzc5awnA- 1533ms linear infinite both;
101
+ -webkit-animation-delay: 0s, -766.5ms;
102
+ animation-delay: 0s, -766.5ms;
103
+ }
104
+
105
+ /*
106
+ * Small renders as a simple fixed-length arc spun only by the SVG
107
+ * (Layer 1). The 8-phase rotation (Layer 2) and the tail-chasing expand
108
+ * (Layer 3) read as too busy at this size, so both are disabled and the
109
+ * arc is given a constant length instead. It also spins ~1.8× faster
110
+ * (1803ms ÷ 1.8 ≈ 1000ms) so the simpler single arc still feels lively.
111
+ */
112
+
113
+ ._888S5Y2IeZ8- .Av9z-aLocss- {
114
+ -webkit-animation-duration: 1000ms;
115
+ animation-duration: 1000ms;
105
116
  }
106
117
 
107
118
  ._888S5Y2IeZ8- ._9XO0290QNCY- {
108
119
  stroke-width: 6;
120
+ stroke-dasharray: 40, 200;
121
+ stroke-dashoffset: 0;
122
+ -webkit-animation: none;
123
+ animation: none;
109
124
  }
110
125
 
111
126
  @-webkit-keyframes WvHFS--Nm80- {
@@ -196,38 +211,53 @@
196
211
 
197
212
  /*
198
213
  * Layer 3 — expand/contract the visible arc length via stroke-dasharray
199
- * while shifting it around the ring via stroke-dashoffset. The "200" gap
200
- * value exceeds the circle's circumference, ensuring only one stroke
201
- * segment is visible at a time.
214
+ * while sliding it around the ring via stroke-dashoffset.
215
+ *
216
+ * Rhythm (cycle = 4 × arc, so one breath = two rotation phases):
217
+ * - bloom open 0 → 50% (one full rotation phase)
218
+ * - deflate 50 → 80% (tail catches the head)
219
+ * - hold 80 → 100% (a beat before re-blooming)
220
+ *
221
+ * The minimum length stays at 8 (not ~0) so the arc never pinches to a dot,
222
+ * and the 100% keyframe matches 0% with the offset advanced one full
223
+ * circumference (≈125.66) so the loop is seamless — no catch-up blink.
202
224
  */
203
225
 
204
226
  @-webkit-keyframes _0m0Nzc5awnA- {
205
227
  0% {
206
- stroke-dasharray: 1, 200;
228
+ stroke-dasharray: 8, 200;
207
229
  stroke-dashoffset: 0;
208
230
  }
209
231
  50% {
210
232
  stroke-dasharray: 90, 200;
211
233
  stroke-dashoffset: -35;
212
234
  }
235
+ 80% {
236
+ stroke-dasharray: 8, 200;
237
+ stroke-dashoffset: -125.66;
238
+ }
213
239
  100% {
214
- stroke-dasharray: 90, 200;
215
- stroke-dashoffset: -125;
240
+ stroke-dasharray: 8, 200;
241
+ stroke-dashoffset: -125.66;
216
242
  }
217
243
  }
218
244
 
219
245
  @keyframes _0m0Nzc5awnA- {
220
246
  0% {
221
- stroke-dasharray: 1, 200;
247
+ stroke-dasharray: 8, 200;
222
248
  stroke-dashoffset: 0;
223
249
  }
224
250
  50% {
225
251
  stroke-dasharray: 90, 200;
226
252
  stroke-dashoffset: -35;
227
253
  }
254
+ 80% {
255
+ stroke-dasharray: 8, 200;
256
+ stroke-dashoffset: -125.66;
257
+ }
228
258
  100% {
229
- stroke-dasharray: 90, 200;
230
- stroke-dashoffset: -125;
259
+ stroke-dasharray: 8, 200;
260
+ stroke-dashoffset: -125.66;
231
261
  }
232
262
  }
233
263
 
@@ -138,6 +138,9 @@
138
138
  "LightBox.Overlay",
139
139
  "LightBox.Provider",
140
140
  "LightBox.Slides",
141
+ "LightBox.Thumbnail",
142
+ "LightBox.ThumbnailBar",
143
+ "LightBox.ThumbnailImage",
141
144
  "LightBox.Thumbnails",
142
145
  "LightBox.Toolbar",
143
146
  "Link",
@@ -226,4 +229,4 @@
226
229
  "Tooltip",
227
230
  "Typography"
228
231
  ]
229
- }
232
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jobber/components",
3
- "version": "8.8.0",
3
+ "version": "8.9.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -556,5 +556,5 @@
556
556
  "> 1%",
557
557
  "IE 10"
558
558
  ],
559
- "gitHead": "954f61ec1d4a5015f706979a120c55af6115a711"
559
+ "gitHead": "e38cc8fc9387c2d0152349bfed2b16cfddb31037"
560
560
  }