@ntbjs/react-components 2.0.18 โ†’ 2.0.20-rc.1

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/README.md CHANGED
@@ -98,27 +98,54 @@ You can build the library with:
98
98
  npm run build
99
99
  ```
100
100
 
101
- ### ๐Ÿงช Installing the package locally (for testing only)
101
+ ### ๐Ÿงช Testing locally with yalc
102
102
 
103
- Let's say you have another project (`test-app`) on your machine that you want to try installing the component library
104
- into without having to first publish the component library. In the `test-app` directory, you can run:
103
+ [yalc](https://github.com/wclr/yalc) simulates publishing the package to a local store, avoiding the duplicate-React
104
+ and symlink issues that `npm link` / `file:` installs often cause.
105
105
 
106
- ```
107
- npm i --save ../path/to/react-components
108
- ```
109
-
110
- which will install the local component library as a dependency in `test-app`. It'll then appear as a dependency
111
- in `package.json` like:
106
+ Install yalc globally once:
112
107
 
113
- ```JSON
114
- "dependencies": {
115
- ...
116
- "@ntbjs/react-components": "file:../path/to/react-components",
117
- ...
118
- }
119
- ```
108
+ ```
109
+ npm i -g yalc
110
+ ```
120
111
 
121
- The library with its utilities and components can then be imported and used in that project.
112
+ #### In `@ntbjs/react-components` (this repo)
113
+
114
+ 1. Build the library:
115
+ ```
116
+ npm run build
117
+ ```
118
+ 2. Publish it to the local yalc store:
119
+ ```
120
+ yalc publish
121
+ ```
122
+ 3. After making further changes, rebuild and push the update to every consumer app that uses it:
123
+ ```
124
+ npm run build && yalc push
125
+ ```
126
+
127
+ #### In the consumer app
128
+
129
+ 1. Add the locally published package:
130
+ ```
131
+ yalc add @ntbjs/react-components
132
+ ```
133
+ This copies the package into `.yalc/` and updates `package.json` to
134
+ `"@ntbjs/react-components": "file:.yalc/@ntbjs/react-components"`.
135
+ 2. Install dependencies:
136
+ ```
137
+ npm install
138
+ ```
139
+ 3. Start the app as usual. Updates sent with `yalc push` from the library are picked up automatically (restart the dev
140
+ server or clear its cache if changes don't show up).
141
+ 4. When done testing, restore the registry version and clean up:
142
+ ```
143
+ yalc remove @ntbjs/react-components
144
+ npm install
145
+ ```
146
+
147
+ **Note:** Don't commit `.yalc/`, `yalc.lock` or the `file:.yalc/...` change in `package.json` โ€” consider adding `.yalc`
148
+ and `yalc.lock` to the consumer app's `.gitignore`.
122
149
 
123
150
  ### ๐Ÿ“ฆ Publishing
124
151
 
@@ -0,0 +1,17 @@
1
+ import * as React from 'react';
2
+
3
+ var _path;
4
+ function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
5
+ function SvgArrowDropDown(props) {
6
+ return /*#__PURE__*/React.createElement("svg", _extends({
7
+ fill: "currentColor",
8
+ "data-name": "Layer 1",
9
+ xmlns: "http://www.w3.org/2000/svg",
10
+ viewBox: "0 0 8 4"
11
+ }, props), _path || (_path = /*#__PURE__*/React.createElement("path", {
12
+ d: "M4 4L0 0h8L4 4z"
13
+ })));
14
+ }
15
+
16
+ export { SvgArrowDropDown as ReactComponent };
17
+ //# sourceMappingURL=arrow-drop-down.svg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"arrow-drop-down.svg.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,34 @@
1
+ import React__default from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Cell as Cell$1 } from './Cell.styled.js';
4
+
5
+ const Cell = React__default.memo(function Cell({
6
+ itemId,
7
+ isActive = false,
8
+ isTabStop = false,
9
+ label,
10
+ onSelect,
11
+ children
12
+ }) {
13
+ return React__default.createElement(Cell$1, {
14
+ type: "button",
15
+ role: "option",
16
+ "aria-selected": isActive,
17
+ "aria-label": label,
18
+ tabIndex: isTabStop ? 0 : -1,
19
+ "data-item-id": itemId,
20
+ $isActive: isActive,
21
+ onClick: () => onSelect?.(itemId)
22
+ }, children);
23
+ });
24
+ Cell.propTypes = process.env.NODE_ENV !== "production" ? {
25
+ itemId: PropTypes.string.isRequired,
26
+ isActive: PropTypes.bool,
27
+ isTabStop: PropTypes.bool,
28
+ label: PropTypes.string,
29
+ onSelect: PropTypes.func,
30
+ children: PropTypes.node
31
+ } : {};
32
+
33
+ export { Cell as default };
34
+ //# sourceMappingURL=Cell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Cell.js","sources":["../../../../src/components/widgets/Filmstrip/Cell/Cell.js"],"sourcesContent":["import React from 'react';\nimport PropTypes from 'prop-types';\nimport * as S from './Cell.styled';\n\n/**\n * One slot in the Filmstrip. Internal: it only knows whether it is active\n * and reports when it is chosen.\n */\n// No forwardRef: nothing outside needs a ref to a single cell yet.\n// memo: a cell re-renders only when its own props change, not on every selection.\nconst Cell = React.memo(function Cell({\n itemId,\n isActive = false,\n isTabStop = false,\n label,\n onSelect,\n children\n}) {\n return (\n <S.Cell\n type=\"button\"\n role=\"option\"\n aria-selected={isActive}\n // Named by the item's label, not by its content, so every option has a name\n aria-label={label}\n // Roving tabindex: only one cell is in the Tab order; arrows move between cells\n tabIndex={isTabStop ? 0 : -1}\n data-item-id={itemId}\n $isActive={isActive}\n onClick={() => onSelect?.(itemId)}\n >\n {children}\n </S.Cell>\n );\n});\n\nCell.propTypes = {\n itemId: PropTypes.string.isRequired,\n isActive: PropTypes.bool,\n isTabStop: PropTypes.bool,\n label: PropTypes.string,\n onSelect: PropTypes.func,\n children: PropTypes.node\n};\n\nexport default Cell;\n"],"names":["Cell","React","memo","itemId","isActive","isTabStop","label","onSelect","children","createElement","S","type","role","tabIndex","$isActive","onClick","propTypes","process","env","NODE_ENV","PropTypes","string","isRequired","bool","func","node"],"mappings":";;;;AAUMA,MAAAA,IAAI,GAAGC,cAAK,CAACC,IAAI,CAAC,SAASF,IAAIA,CAAC;EACpCG,MAAM;AACNC,EAAAA,QAAQ,GAAG,KAAK;AAChBC,EAAAA,SAAS,GAAG,KAAK;EACjBC,KAAK;EACLC,QAAQ;AACRC,EAAAA,QAAAA;AACF,CAAC,EAAE;AACD,EAAA,OACEP,cAAA,CAAAQ,aAAA,CAACC,MAAM,EAAA;AACLC,IAAAA,IAAI,EAAC,QAAQ;AACbC,IAAAA,IAAI,EAAC,QAAQ;AACb,IAAA,eAAA,EAAeR,QAAS;AAExB,IAAA,YAAA,EAAYE,KAAM;AAElBO,IAAAA,QAAQ,EAAER,SAAS,GAAG,CAAC,GAAG,CAAC,CAAE;AAC7B,IAAA,cAAA,EAAcF,MAAO;AACrBW,IAAAA,SAAS,EAAEV,QAAS;AACpBW,IAAAA,OAAO,EAAEA,MAAMR,QAAQ,GAAGJ,MAAM,CAAA;AAAE,GAAA,EAEjCK,QACK,CAAC,CAAA;AAEb,CAAC,EAAC;AAEFR,IAAI,CAACgB,SAAS,GAAAC,OAAA,CAAAC,GAAA,CAAAC,QAAA,KAAG,YAAA,GAAA;AACfhB,EAAAA,MAAM,EAAEiB,SAAS,CAACC,MAAM,CAACC,UAAU;EACnClB,QAAQ,EAAEgB,SAAS,CAACG,IAAI;EACxBlB,SAAS,EAAEe,SAAS,CAACG,IAAI;EACzBjB,KAAK,EAAEc,SAAS,CAACC,MAAM;EACvBd,QAAQ,EAAEa,SAAS,CAACI,IAAI;EACxBhB,QAAQ,EAAEY,SAAS,CAACK,IAAAA;AACtB,CAAC,GAAA,EAAA;;;;"}
@@ -0,0 +1,46 @@
1
+ import styled from 'styled-components';
2
+ import { applyDefaultTheme } from '../../../utils/defaultTheme.js';
3
+
4
+ const Cell = styled.button.attrs(applyDefaultTheme)`
5
+ /* anchor for the ring layer below */
6
+ position: relative;
7
+ flex: none;
8
+ width: 96px;
9
+ height: 72px;
10
+ padding: 0;
11
+ border: none;
12
+ background: ${props => props.theme.colors.neutral10};
13
+ cursor: pointer;
14
+
15
+ /* Orange, not the theme's primary green: green reads as "success / action done",
16
+ while this ring only marks where you are. Lighter shade in dark mode for contrast. */
17
+ ${props => props.theme.themeProp('--filmstrip-ring-color', props.theme.getColor('orange-400'), props.theme.getColor('orange-500'))}
18
+
19
+ /* The active ring is its own layer on top of the content. An outline on the button
20
+ itself gets covered by positioned children (Thumbnail is position: relative for the
21
+ play pill), so any renderItem could hide it. An inset box-shadow never changes
22
+ the cell size, and pointer-events: none keeps clicks going to the button. */
23
+ &::after {
24
+ content: '';
25
+ position: absolute;
26
+ inset: 0;
27
+ z-index: 1;
28
+ pointer-events: none;
29
+ box-shadow: ${props => props.$isActive ? 'inset 0 0 0 4px var(--filmstrip-ring-color)' : 'none'};
30
+ }
31
+
32
+ /* Keyboard navigation moves focus with the selection, so the browser's default focus
33
+ ring would draw a second line around the active ring. The ring layer shows focus. */
34
+ &:focus {
35
+ outline: none;
36
+ }
37
+
38
+ /* A focused cell that isn't active yet (e.g. tabbing in with nothing selected)
39
+ still needs a visible indicator for keyboard users. */
40
+ &:focus-visible::after {
41
+ box-shadow: inset 0 0 0 4px var(--filmstrip-ring-color);
42
+ }
43
+ `;
44
+
45
+ export { Cell };
46
+ //# sourceMappingURL=Cell.styled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Cell.styled.js","sources":["../../../../src/components/widgets/Filmstrip/Cell/Cell.styled.js"],"sourcesContent":["import styled from 'styled-components';\n// Fix: Cell/ is one folder deeper than Filmstrip/, so it needs one more `../`.\nimport { applyDefaultTheme } from '../../../../utils/defaultTheme';\n\n// Was `styled.p`. A button is focusable and clickable by default.\n// `$isActive` is a transient prop, so it never reaches the DOM.\nexport const Cell = styled.button.attrs(applyDefaultTheme)`\n /* anchor for the ring layer below */\n position: relative;\n flex: none;\n width: 96px;\n height: 72px;\n padding: 0;\n border: none;\n background: ${props => props.theme.colors.neutral10};\n cursor: pointer;\n\n /* Orange, not the theme's primary green: green reads as \"success / action done\",\n while this ring only marks where you are. Lighter shade in dark mode for contrast. */\n ${props =>\n props.theme.themeProp(\n '--filmstrip-ring-color',\n props.theme.getColor('orange-400'),\n props.theme.getColor('orange-500')\n )}\n\n /* The active ring is its own layer on top of the content. An outline on the button\n itself gets covered by positioned children (Thumbnail is position: relative for the\n play pill), so any renderItem could hide it. An inset box-shadow never changes\n the cell size, and pointer-events: none keeps clicks going to the button. */\n &::after {\n content: '';\n position: absolute;\n inset: 0;\n z-index: 1;\n pointer-events: none;\n box-shadow: ${props =>\n props.$isActive ? 'inset 0 0 0 4px var(--filmstrip-ring-color)' : 'none'};\n }\n\n /* Keyboard navigation moves focus with the selection, so the browser's default focus\n ring would draw a second line around the active ring. The ring layer shows focus. */\n &:focus {\n outline: none;\n }\n\n /* A focused cell that isn't active yet (e.g. tabbing in with nothing selected)\n still needs a visible indicator for keyboard users. */\n &:focus-visible::after {\n box-shadow: inset 0 0 0 4px var(--filmstrip-ring-color);\n }\n`;\n"],"names":["Cell","styled","button","attrs","applyDefaultTheme","props","theme","colors","neutral10","themeProp","getColor","$isActive"],"mappings":";;;AAMO,MAAMA,IAAI,GAAGC,MAAM,CAACC,MAAM,CAACC,KAAK,CAACC,iBAAiB,CAAC,CAAA;AAC1D;AACA;AACA;AACA;AACA;AACA;AACA;AACA,cAAgBC,EAAAA,KAAK,IAAIA,KAAK,CAACC,KAAK,CAACC,MAAM,CAACC,SAAS,CAAA;AACrD;AACA;AACA;AACA;AACA,EAAIH,EAAAA,KAAK,IACLA,KAAK,CAACC,KAAK,CAACG,SAAS,CACnB,wBAAwB,EACxBJ,KAAK,CAACC,KAAK,CAACI,QAAQ,CAAC,YAAY,CAAC,EAClCL,KAAK,CAACC,KAAK,CAACI,QAAQ,CAAC,YAAY,CACnC,CAAC,CAAA;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,gBAAkBL,EAAAA,KAAK,IACjBA,KAAK,CAACM,SAAS,GAAG,6CAA6C,GAAG,MAAM,CAAA;AAC9E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;"}
@@ -0,0 +1,240 @@
1
+ import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
2
+ import React__default, { useRef, useCallback, useState, useEffect } from 'react';
3
+ import PropTypes from 'prop-types';
4
+ import { ReactComponent as SvgArrowDropDown } from '../../icons/arrow-drop-down.svg.js';
5
+ import { Wrapper, ToggleBar, ToggleButton, ToggleIcon, Collapse, CollapseContent, Row, ScrollButton, Filmstrip as Filmstrip$1, LoadingCell, Spinner, LoadMoreSentinel } from './Filmstrip.styled.js';
6
+ import Cell from './Cell/Cell.js';
7
+ import Thumbnail from './Thumbnail/Thumbnail.js';
8
+
9
+ const defaultRenderItem = item => React__default.createElement(Thumbnail, {
10
+ thumbnailUrl: item.thumbnailUrl,
11
+ alt: item.alt,
12
+ fileType: item.fileType,
13
+ duration: item.duration
14
+ });
15
+ const EMPTY_ITEMS = [];
16
+ let idCounter = 0;
17
+ const useFallbackId = () => {
18
+ const idRef = useRef(null);
19
+ if (idRef.current === null) {
20
+ idCounter += 1;
21
+ idRef.current = `filmstrip-${idCounter}`;
22
+ }
23
+ return idRef.current;
24
+ };
25
+ const useStripId = React__default.useId || useFallbackId;
26
+ const Filmstrip = React__default.forwardRef(function Filmstrip({
27
+ items = EMPTY_ITEMS,
28
+ activeId,
29
+ onActiveChange,
30
+ renderItem = defaultRenderItem,
31
+ collapsible = false,
32
+ expanded,
33
+ defaultExpanded = true,
34
+ onExpandedChange,
35
+ onLoadMore,
36
+ hasMore = false,
37
+ isLoading = false,
38
+ loadMoreThreshold = 300,
39
+ showScrollButtons = false,
40
+ onKeyDown: onKeyDownProp,
41
+ id,
42
+ ...props
43
+ }, forwardedRef) {
44
+ const generatedId = useStripId();
45
+ const stripId = id ?? generatedId;
46
+ const stripRef = useRef(null);
47
+ const setStripRef = useCallback(node => {
48
+ stripRef.current = node;
49
+ if (typeof forwardedRef === 'function') forwardedRef(node);else if (forwardedRef) forwardedRef.current = node;
50
+ }, [forwardedRef]);
51
+ const sentinelRef = useRef(null);
52
+ const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);
53
+ const isControlled = expanded !== undefined;
54
+ const isExpanded = isControlled ? expanded : internalExpanded;
55
+ const isVisible = !collapsible || isExpanded;
56
+ const onToggle = () => {
57
+ const nextExpanded = !isExpanded;
58
+ if (!isControlled) setInternalExpanded(nextExpanded);
59
+ onExpandedChange?.(nextExpanded);
60
+ };
61
+ useEffect(() => {
62
+ const root = stripRef.current;
63
+ const sentinel = sentinelRef.current;
64
+ if (!onLoadMore || !hasMore || isLoading || !isVisible || !root || !sentinel) {
65
+ return undefined;
66
+ }
67
+ if (typeof IntersectionObserver === 'undefined') return undefined;
68
+ const observer = new IntersectionObserver(entries => {
69
+ if (entries[0].isIntersecting) onLoadMore();
70
+ }, {
71
+ root,
72
+ rootMargin: `0px ${loadMoreThreshold}px 0px 0px`
73
+ });
74
+ observer.observe(sentinel);
75
+ return () => observer.disconnect();
76
+ }, [items.length, onLoadMore, hasMore, isLoading, isVisible, loadMoreThreshold]);
77
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
78
+ const [canScrollRight, setCanScrollRight] = useState(false);
79
+ useEffect(() => {
80
+ const strip = stripRef.current;
81
+ if (!showScrollButtons || !isVisible || !strip) return undefined;
82
+ const updateEdges = () => {
83
+ setCanScrollLeft(strip.scrollLeft > 1);
84
+ setCanScrollRight(strip.scrollLeft + strip.clientWidth < strip.scrollWidth - 1);
85
+ };
86
+ updateEdges();
87
+ strip.addEventListener('scroll', updateEdges, {
88
+ passive: true
89
+ });
90
+ const resizeObserver = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(updateEdges);
91
+ resizeObserver?.observe(strip);
92
+ return () => {
93
+ strip.removeEventListener('scroll', updateEdges);
94
+ resizeObserver?.disconnect();
95
+ };
96
+ }, [showScrollButtons, isVisible, items.length, isLoading]);
97
+ const scrollPage = direction => {
98
+ const strip = stripRef.current;
99
+ if (!strip) return;
100
+ const view = strip.getBoundingClientRect();
101
+ const cells = Array.from(strip.querySelectorAll('[data-item-id]'));
102
+ let target;
103
+ if (direction === 'right') {
104
+ const edgeCell = [...cells].reverse().find(cell => cell.getBoundingClientRect().left < view.right - 1);
105
+ const offset = edgeCell ? edgeCell.getBoundingClientRect().left - view.left : 0;
106
+ target = strip.scrollLeft + (offset > 1 ? offset : strip.clientWidth);
107
+ } else {
108
+ const edgeCell = cells.find(cell => cell.getBoundingClientRect().right > view.left + 1);
109
+ const offset = edgeCell ? view.right - edgeCell.getBoundingClientRect().right : 0;
110
+ target = strip.scrollLeft - (offset > 1 ? offset : strip.clientWidth);
111
+ }
112
+ const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
113
+ strip.scrollTo({
114
+ left: target,
115
+ behavior: reduceMotion ? 'auto' : 'smooth'
116
+ });
117
+ };
118
+ const onKeyDown = event => {
119
+ onKeyDownProp?.(event);
120
+ if (event.defaultPrevented || !items.length) return;
121
+ const currentIndex = items.findIndex(item => item.id === activeId);
122
+ let nextIndex;
123
+ switch (event.key) {
124
+ case 'ArrowRight':
125
+ nextIndex = currentIndex < 0 ? 0 : Math.min(currentIndex + 1, items.length - 1);
126
+ break;
127
+ case 'ArrowLeft':
128
+ nextIndex = currentIndex < 0 ? 0 : Math.max(currentIndex - 1, 0);
129
+ break;
130
+ case 'Home':
131
+ nextIndex = 0;
132
+ break;
133
+ case 'End':
134
+ nextIndex = items.length - 1;
135
+ break;
136
+ default:
137
+ return;
138
+ }
139
+ event.preventDefault();
140
+ if (nextIndex === currentIndex) return;
141
+ const nextId = items[nextIndex].id;
142
+ onActiveChange?.(nextId);
143
+ const cell = stripRef.current?.querySelector(`[data-item-id="${CSS.escape(nextId)}"]`);
144
+ if (cell) {
145
+ cell.focus({
146
+ preventScroll: true
147
+ });
148
+ cell.scrollIntoView?.({
149
+ block: 'nearest',
150
+ inline: 'nearest',
151
+ behavior: 'smooth'
152
+ });
153
+ }
154
+ };
155
+ const hasActiveItem = items.some(item => item.id === activeId);
156
+ return (React__default.createElement(Wrapper, null, collapsible && React__default.createElement(ToggleBar, null, React__default.createElement(ToggleButton, {
157
+ type: "button",
158
+ "aria-expanded": isExpanded,
159
+ "aria-controls": stripId,
160
+ "aria-label": isExpanded ? 'Hide filmstrip' : 'Show filmstrip',
161
+ onClick: onToggle
162
+ }, React__default.createElement(ToggleIcon, {
163
+ $isExpanded: isExpanded
164
+ }, React__default.createElement(SvgArrowDropDown, {
165
+ "aria-hidden": "true"
166
+ })))), React__default.createElement(Collapse, {
167
+ $isExpanded: isVisible
168
+ }, React__default.createElement(CollapseContent, null, React__default.createElement(Row, null, showScrollButtons && React__default.createElement(ScrollButton, {
169
+ type: "button",
170
+ $direction: "left",
171
+ "aria-label": "Scroll filmstrip left",
172
+ "aria-controls": stripId,
173
+ disabled: !canScrollLeft,
174
+ onClick: () => scrollPage('left')
175
+ }, React__default.createElement(SvgArrowDropDown, {
176
+ "aria-hidden": "true"
177
+ })), React__default.createElement(Filmstrip$1, _extends({
178
+ ref: setStripRef,
179
+ id: stripId,
180
+ role: "listbox",
181
+ "aria-orientation": "horizontal",
182
+ "aria-busy": isLoading
183
+ }, props, {
184
+ onKeyDown: onKeyDown
185
+ }), items.map((item, index) => {
186
+ const isActive = item.id === activeId;
187
+ return (React__default.createElement(Cell, {
188
+ key: item.id,
189
+ itemId: item.id,
190
+ isActive: isActive,
191
+ isTabStop: hasActiveItem ? isActive : index === 0,
192
+ label: item.alt || `Asset ${index + 1}`,
193
+ onSelect: onActiveChange
194
+ }, renderItem(item, {
195
+ isActive
196
+ }))
197
+ );
198
+ }), isLoading && React__default.createElement(LoadingCell, {
199
+ "aria-hidden": "true"
200
+ }, React__default.createElement(Spinner, null)), onLoadMore && hasMore && React__default.createElement(LoadMoreSentinel, {
201
+ ref: sentinelRef,
202
+ "aria-hidden": "true"
203
+ })), showScrollButtons && React__default.createElement(ScrollButton, {
204
+ type: "button",
205
+ $direction: "right",
206
+ "aria-label": "Scroll filmstrip right",
207
+ "aria-controls": stripId,
208
+ disabled: !canScrollRight,
209
+ onClick: () => scrollPage('right')
210
+ }, React__default.createElement(SvgArrowDropDown, {
211
+ "aria-hidden": "true"
212
+ }))))))
213
+ );
214
+ });
215
+ Filmstrip.propTypes = process.env.NODE_ENV !== "production" ? {
216
+ items: PropTypes.arrayOf(PropTypes.shape({
217
+ id: PropTypes.string.isRequired,
218
+ thumbnailUrl: PropTypes.string,
219
+ alt: PropTypes.string.isRequired,
220
+ fileType: PropTypes.oneOf(['image', 'video', 'pdf', 'file', 'audio']),
221
+ duration: PropTypes.number
222
+ })),
223
+ activeId: PropTypes.string,
224
+ onActiveChange: PropTypes.func,
225
+ renderItem: PropTypes.func,
226
+ collapsible: PropTypes.bool,
227
+ expanded: PropTypes.bool,
228
+ defaultExpanded: PropTypes.bool,
229
+ onExpandedChange: PropTypes.func,
230
+ onLoadMore: PropTypes.func,
231
+ hasMore: PropTypes.bool,
232
+ isLoading: PropTypes.bool,
233
+ loadMoreThreshold: PropTypes.number,
234
+ showScrollButtons: PropTypes.bool,
235
+ id: PropTypes.string,
236
+ onKeyDown: PropTypes.func
237
+ } : {};
238
+
239
+ export { Filmstrip as default };
240
+ //# sourceMappingURL=Filmstrip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Filmstrip.js","sources":["../../../src/components/widgets/Filmstrip/Filmstrip.js"],"sourcesContent":["import React, { useCallback, useEffect, useRef, useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { ArrowDropDownIcon } from '../../../icons';\nimport * as S from './Filmstrip.styled';\nimport Cell from './Cell';\nimport Thumbnail from './Thumbnail';\n\nconst defaultRenderItem = item => (\n <Thumbnail\n thumbnailUrl={item.thumbnailUrl}\n alt={item.alt}\n fileType={item.fileType}\n duration={item.duration}\n />\n);\n\nconst EMPTY_ITEMS = [];\n\n// The toggle button needs the strip's id for aria-controls. React.useId only exists from\n// React 18, and this library supports 16.8+, so older versions fall back to a counter.\n// Picking the hook once, at module level, keeps the hook order the same on every render.\nlet idCounter = 0;\nconst useFallbackId = () => {\n const idRef = useRef(null);\n if (idRef.current === null) {\n idCounter += 1;\n idRef.current = `filmstrip-${idCounter}`;\n }\n return idRef.current;\n};\nconst useStripId = React.useId || useFallbackId;\n\n/**\n * Horizontal strip of thumbnails. Controlled: the parent owns `activeId`\n * and updates it from `onActiveChange`.\n *\n * With `collapsible`, a bar with a centered arrow above the strip hides and shows it.\n * The expanded state works both ways:\n * - controlled: pass `expanded` and update it from `onExpandedChange` (e.g. keep it in Redux)\n * - uncontrolled: leave `expanded` out; the Filmstrip keeps its own state, starting at `defaultExpanded`\n *\n * Loading more: the Filmstrip never fetches. When the user scrolls near the end it calls\n * `onLoadMore`; the parent fetches the next page and appends it to `items`. `hasMore={false}`\n * stops further calls, and `isLoading` prevents duplicate requests while a fetch runs.\n */\nconst Filmstrip = React.forwardRef(function Filmstrip(\n {\n items = EMPTY_ITEMS,\n activeId,\n onActiveChange,\n renderItem = defaultRenderItem,\n collapsible = false,\n expanded,\n defaultExpanded = true,\n onExpandedChange,\n onLoadMore,\n hasMore = false,\n isLoading = false,\n loadMoreThreshold = 300,\n showScrollButtons = false,\n onKeyDown: onKeyDownProp,\n id,\n ...props\n },\n forwardedRef\n) {\n // Always call the hook (hook order), but a parent's `id` wins. The toggle and page\n // buttons use the same value for aria-controls, so they keep pointing at the strip.\n const generatedId = useStripId();\n const stripId = id ?? generatedId;\n\n // The Filmstrip needs its own ref to the scroll container (as the observer's root),\n // but the parent may also pass a ref. An element takes only one `ref`, so this\n // callback ref fills both: ours, and the parent's (function or object ref).\n const stripRef = useRef(null);\n const setStripRef = useCallback(\n node => {\n stripRef.current = node;\n if (typeof forwardedRef === 'function') forwardedRef(node);\n else if (forwardedRef) forwardedRef.current = node;\n },\n [forwardedRef]\n );\n\n // An invisible element after the last cell. When it scrolls into view, the end is near.\n const sentinelRef = useRef(null);\n\n // Own state, only used when the parent doesn't control `expanded`\n const [internalExpanded, setInternalExpanded] = useState(defaultExpanded);\n // `expanded` given (true or false) = controlled. Left out (undefined) = uncontrolled.\n const isControlled = expanded !== undefined;\n const isExpanded = isControlled ? expanded : internalExpanded;\n // A strip that can't collapse is always visible, whatever `expanded` says\n const isVisible = !collapsible || isExpanded;\n\n const onToggle = () => {\n const nextExpanded = !isExpanded;\n // Uncontrolled: update our own state. Controlled: only report, the parent decides.\n if (!isControlled) setInternalExpanded(nextExpanded);\n onExpandedChange?.(nextExpanded);\n };\n\n // Asks the parent for more items when the sentinel comes within `loadMoreThreshold`\n // pixels of the strip's right edge. IntersectionObserver lets the browser do the\n // watching, so there's no scroll listener running on every scroll event.\n useEffect(() => {\n const root = stripRef.current;\n const sentinel = sentinelRef.current;\n // Nothing to do: no callback, no more pages, a fetch already running, or collapsed\n if (!onLoadMore || !hasMore || isLoading || !isVisible || !root || !sentinel) {\n return undefined;\n }\n // Not available in old browsers or in jsdom (Jest)\n if (typeof IntersectionObserver === 'undefined') return undefined;\n\n const observer = new IntersectionObserver(\n entries => {\n if (entries[0].isIntersecting) onLoadMore();\n },\n // A positive right margin grows the root, so loading starts before the real end\n { root, rootMargin: `0px ${loadMoreThreshold}px 0px 0px` }\n );\n observer.observe(sentinel);\n return () => observer.disconnect();\n // items.length: a new observer after each page checks again right away. If the new\n // page didn't fill the strip, the sentinel is still in view and the next page loads.\n }, [items.length, onLoadMore, hasMore, isLoading, isVisible, loadMoreThreshold]);\n\n // Page buttons: whether there is anything left to scroll on each side. Updated on\n // scroll, on resize (ResizeObserver) and when items are added.\n const [canScrollLeft, setCanScrollLeft] = useState(false);\n const [canScrollRight, setCanScrollRight] = useState(false);\n\n useEffect(() => {\n const strip = stripRef.current;\n if (!showScrollButtons || !isVisible || !strip) return undefined;\n\n const updateEdges = () => {\n // 1px tolerance: scrollLeft can be fractional on zoomed or high-DPI screens\n setCanScrollLeft(strip.scrollLeft > 1);\n setCanScrollRight(strip.scrollLeft + strip.clientWidth < strip.scrollWidth - 1);\n };\n updateEdges();\n\n strip.addEventListener('scroll', updateEdges, { passive: true });\n const resizeObserver =\n typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(updateEdges);\n resizeObserver?.observe(strip);\n return () => {\n strip.removeEventListener('scroll', updateEdges);\n resizeObserver?.disconnect();\n };\n }, [showScrollButtons, isVisible, items.length, isLoading]);\n\n // Scrolls one page. Right: the last (even partly) visible cell becomes the first one.\n // Left: the first visible cell becomes the last one. The selection doesn't change.\n const scrollPage = direction => {\n const strip = stripRef.current;\n if (!strip) return;\n const view = strip.getBoundingClientRect();\n const cells = Array.from(strip.querySelectorAll('[data-item-id]'));\n // Rectangles instead of offsetLeft: the strip has no CSS position, so offsetLeft\n // would be measured from some ancestor instead of the strip.\n let target;\n if (direction === 'right') {\n // Last cell that starts before the right edge\n const edgeCell = [...cells]\n .reverse()\n .find(cell => cell.getBoundingClientRect().left < view.right - 1);\n const offset = edgeCell ? edgeCell.getBoundingClientRect().left - view.left : 0;\n // A cell already at the left edge (or none found) would mean no movement: page by width\n target = strip.scrollLeft + (offset > 1 ? offset : strip.clientWidth);\n } else {\n // First cell that ends after the left edge\n const edgeCell = cells.find(cell => cell.getBoundingClientRect().right > view.left + 1);\n const offset = edgeCell ? view.right - edgeCell.getBoundingClientRect().right : 0;\n target = strip.scrollLeft - (offset > 1 ? offset : strip.clientWidth);\n }\n // The browser clamps at both ends, so the last page simply stops at the end\n const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n strip.scrollTo({ left: target, behavior: reduceMotion ? 'auto' : 'smooth' });\n };\n\n // Keyboard navigation: with a cell focused (e.g. after clicking it), the arrow keys\n // select the previous/next asset, Home/End jump to the first/last. Focus follows the\n // selection so the next key press continues from there, and the cell scrolls into view.\n const onKeyDown = event => {\n onKeyDownProp?.(event);\n if (event.defaultPrevented || !items.length) return;\n\n const currentIndex = items.findIndex(item => item.id === activeId);\n let nextIndex;\n switch (event.key) {\n case 'ArrowRight':\n nextIndex = currentIndex < 0 ? 0 : Math.min(currentIndex + 1, items.length - 1);\n break;\n case 'ArrowLeft':\n nextIndex = currentIndex < 0 ? 0 : Math.max(currentIndex - 1, 0);\n break;\n case 'Home':\n nextIndex = 0;\n break;\n case 'End':\n nextIndex = items.length - 1;\n break;\n default:\n return;\n }\n // Stop the browser from scrolling the strip/page with the arrow keys\n event.preventDefault();\n if (nextIndex === currentIndex) return;\n\n const nextId = items[nextIndex].id;\n onActiveChange?.(nextId);\n\n const cell = stripRef.current?.querySelector(`[data-item-id=\"${CSS.escape(nextId)}\"]`);\n if (cell) {\n cell.focus({ preventScroll: true });\n cell.scrollIntoView?.({ block: 'nearest', inline: 'nearest', behavior: 'smooth' });\n }\n };\n\n // The Tab stop: the active cell, or the first one when nothing is active yet\n const hasActiveItem = items.some(item => item.id === activeId);\n\n return (\n // The wrapper holds the toggle bar and the strip. The toggle can't live inside the\n // strip: it would scroll away with the thumbnails and sit inside the listbox.\n <S.Wrapper>\n {collapsible && (\n <S.ToggleBar>\n {/* A real button: focusable, works with Enter/Space. aria-expanded tells screen\n readers the current state, aria-controls which element it shows and hides. */}\n <S.ToggleButton\n type=\"button\"\n aria-expanded={isExpanded}\n aria-controls={stripId}\n aria-label={isExpanded ? 'Hide filmstrip' : 'Show filmstrip'}\n onClick={onToggle}\n >\n {/* One icon, rotated with CSS: down = \"hide\", up = \"show\". */}\n <S.ToggleIcon $isExpanded={isExpanded}>\n <ArrowDropDownIcon aria-hidden=\"true\" />\n </S.ToggleIcon>\n </S.ToggleButton>\n </S.ToggleBar>\n )}\n\n {/* Collapse animates the height and slides the strip down (see Filmstrip.styled.js).\n When collapsed it also sets visibility: hidden, so hidden cells can't be tabbed\n to. A non-collapsible strip is always expanded, so this changes nothing for it. */}\n <S.Collapse $isExpanded={isVisible}>\n <S.CollapseContent>\n {/* The ref and rest props (aria-label, ...) stay on the strip, as before, so the\n listbox keeps its label and a parent's ref still points to the scroll container. */}\n <S.Row>\n {showScrollButtons && (\n // Outside the listbox: a button can't be an option. aria-controls links it to the strip.\n <S.ScrollButton\n type=\"button\"\n $direction=\"left\"\n aria-label=\"Scroll filmstrip left\"\n aria-controls={stripId}\n disabled={!canScrollLeft}\n onClick={() => scrollPage('left')}\n >\n <ArrowDropDownIcon aria-hidden=\"true\" />\n </S.ScrollButton>\n )}\n <S.Filmstrip\n ref={setStripRef}\n id={stripId}\n role=\"listbox\"\n aria-orientation=\"horizontal\"\n // Tells assistive technology the list is being updated while a page loads\n aria-busy={isLoading}\n {...props}\n onKeyDown={onKeyDown}\n >\n {items.map((item, index) => {\n const isActive = item.id === activeId;\n\n return (\n // Fix: every element in a mapped list needs a stable `key`.\n <Cell\n key={item.id}\n itemId={item.id}\n isActive={isActive}\n isTabStop={hasActiveItem ? isActive : index === 0}\n // The option's accessible name. It must not depend on the thumbnail:\n // audio and image-less items have no <img> to take a name from.\n label={item.alt || `Asset ${index + 1}`}\n onSelect={onActiveChange}\n >\n {/* The Filmstrip calls renderItem and hands the result to Cell as children.\n No file-type logic here: that lives in Thumbnail (or the parent's renderItem). */}\n {renderItem(item, { isActive })}\n </Cell>\n );\n })}\n {/* A spinner cell at the end while the parent fetches the next page. It fades in\n after a short delay (see Filmstrip.styled.js), so a fast response never\n flashes it. aria-hidden: screen readers get aria-busy on the listbox instead. */}\n {isLoading && (\n <S.LoadingCell aria-hidden=\"true\">\n <S.Spinner />\n </S.LoadingCell>\n )}\n {/* Only rendered while there's something left to load. aria-hidden: it's not an\n option, so screen readers skip it inside the listbox. */}\n {onLoadMore && hasMore && <S.LoadMoreSentinel ref={sentinelRef} aria-hidden=\"true\" />}\n </S.Filmstrip>\n {showScrollButtons && (\n <S.ScrollButton\n type=\"button\"\n $direction=\"right\"\n aria-label=\"Scroll filmstrip right\"\n aria-controls={stripId}\n disabled={!canScrollRight}\n onClick={() => scrollPage('right')}\n >\n <ArrowDropDownIcon aria-hidden=\"true\" />\n </S.ScrollButton>\n )}\n </S.Row>\n </S.CollapseContent>\n </S.Collapse>\n </S.Wrapper>\n );\n});\n\nFilmstrip.propTypes = {\n /** The assets to show, in order. */\n items: PropTypes.arrayOf(\n PropTypes.shape({\n id: PropTypes.string.isRequired,\n /** Still image. For videos, the poster image, never the video file. */\n thumbnailUrl: PropTypes.string,\n /** Required: the cell's accessible name (screen readers announce it for each option). */\n alt: PropTypes.string.isRequired,\n /** Used by the default renderer: audio shows an icon, video shows a play pill. */\n fileType: PropTypes.oneOf(['image', 'video', 'pdf', 'file', 'audio']),\n /** Video duration in milliseconds, shown in the play pill. */\n duration: PropTypes.number\n })\n ),\n /** Id of the active item. Owned by the parent (controlled). */\n activeId: PropTypes.string,\n /** Called with the id of the item the user chose. */\n onActiveChange: PropTypes.func,\n /** Renders the content of one cell: (item, { isActive }) => node. Defaults to the thumbnail image. */\n renderItem: PropTypes.func,\n /** Shows a bar with an arrow above the strip that hides and shows it. */\n collapsible: PropTypes.bool,\n /** Controlled: whether the strip is shown. Leave out to let the Filmstrip manage it. */\n expanded: PropTypes.bool,\n /** Uncontrolled: whether the strip starts shown. Ignored when `expanded` is passed. */\n defaultExpanded: PropTypes.bool,\n /** Called with the new value when the user hides or shows the strip. */\n onExpandedChange: PropTypes.func,\n /** Called when the user scrolls near the end. Fetch the next page and append it to `items`. */\n onLoadMore: PropTypes.func,\n /** Whether more items can be loaded. Set to false when the last page came back. */\n hasMore: PropTypes.bool,\n /** True while a page is being fetched, so `onLoadMore` isn't called twice. */\n isLoading: PropTypes.bool,\n /** How many pixels before the end `onLoadMore` is called. */\n loadMoreThreshold: PropTypes.number,\n /** Shows a button on each side that scrolls the strip one page. Doesn't change the selection. */\n showScrollButtons: PropTypes.bool,\n /** Id of the scroll container. Generated when left out. */\n id: PropTypes.string,\n /** Called before the built-in arrow-key handling; call preventDefault() to skip it. */\n onKeyDown: PropTypes.func\n};\n\nexport default Filmstrip;\n"],"names":["defaultRenderItem","item","React","createElement","Thumbnail","thumbnailUrl","alt","fileType","duration","EMPTY_ITEMS","idCounter","useFallbackId","idRef","useRef","current","useStripId","useId","Filmstrip","forwardRef","items","activeId","onActiveChange","renderItem","collapsible","expanded","defaultExpanded","onExpandedChange","onLoadMore","hasMore","isLoading","loadMoreThreshold","showScrollButtons","onKeyDown","onKeyDownProp","id","props","forwardedRef","generatedId","stripId","stripRef","setStripRef","useCallback","node","sentinelRef","internalExpanded","setInternalExpanded","useState","isControlled","undefined","isExpanded","isVisible","onToggle","nextExpanded","useEffect","root","sentinel","IntersectionObserver","observer","entries","isIntersecting","rootMargin","observe","disconnect","length","canScrollLeft","setCanScrollLeft","canScrollRight","setCanScrollRight","strip","updateEdges","scrollLeft","clientWidth","scrollWidth","addEventListener","passive","resizeObserver","ResizeObserver","removeEventListener","scrollPage","direction","view","getBoundingClientRect","cells","Array","from","querySelectorAll","target","edgeCell","reverse","find","cell","left","right","offset","reduceMotion","window","matchMedia","matches","scrollTo","behavior","event","defaultPrevented","currentIndex","findIndex","nextIndex","key","Math","min","max","preventDefault","nextId","querySelector","CSS","escape","focus","preventScroll","scrollIntoView","block","inline","hasActiveItem","some","S","type","onClick","$isExpanded","ArrowDropDownIcon","$direction","disabled","_extends","ref","role","map","index","isActive","Cell","itemId","isTabStop","label","onSelect","propTypes","process","env","NODE_ENV","PropTypes","arrayOf","shape","string","isRequired","oneOf","number","func","bool"],"mappings":"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"}
@@ -0,0 +1,160 @@
1
+ import styled, { css, keyframes } from 'styled-components';
2
+ import { applyDefaultTheme } from '../../utils/defaultTheme.js';
3
+
4
+ const COLLAPSE_DURATION = '350ms';
5
+ const EASE_OUT = 'cubic-bezier(0.22, 1, 0.36, 1)';
6
+ const noMotion = css`
7
+ @media (prefers-reduced-motion: reduce) {
8
+ transition: none;
9
+ }
10
+ `;
11
+ const Wrapper = styled.div.attrs(applyDefaultTheme)`
12
+ display: flex;
13
+ flex-direction: column;
14
+ font-family: ${props => props.theme.primaryFontFamily};
15
+ `;
16
+ const ToggleBar = styled.div`
17
+ display: flex;
18
+ justify-content: center;
19
+ `;
20
+ const ToggleButton = styled.button.attrs(applyDefaultTheme)`
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ width: 48px;
25
+ height: 20px;
26
+ padding: 0;
27
+ border: none;
28
+ background: transparent;
29
+ cursor: pointer;
30
+
31
+ /* The arrow svg uses currentColor, so setting color is enough.
32
+ themeProp(property, dark, light) is the repo's dark mode pattern: it covers the
33
+ body.dark-theme / body.light-theme classes from Storybook's toggle and falls back
34
+ to the system's prefers-color-scheme when neither is set. */
35
+ ${props => props.theme.themeProp('color', props.theme.getColor('gray-300'), props.theme.getColor('gray-600'))}
36
+
37
+ /* Specificity 1 nests the theme selectors under :hover (same as AssetGalleryGridCard). */
38
+ &:hover {
39
+ ${props => props.theme.themeProp('color', props.theme.getColor('gray-100'), props.theme.getColor('gray-900'), 1)}
40
+ }
41
+ `;
42
+ const ToggleIcon = styled.span`
43
+ display: flex;
44
+ transform: rotate(${props => props.$isExpanded ? '0deg' : '180deg'});
45
+ transition: transform ${COLLAPSE_DURATION} ${EASE_OUT};
46
+ ${noMotion}
47
+
48
+ svg {
49
+ width: 24px;
50
+ height: 24px;
51
+ }
52
+ `;
53
+ const Collapse = styled.div`
54
+ display: grid;
55
+ grid-template-rows: ${props => props.$isExpanded ? '1fr' : '0fr'};
56
+ /* visibility: hidden removes collapsed cells from the Tab order and screen readers.
57
+ When collapsing it waits until the slide has finished (the delay); when expanding
58
+ it switches back to visible immediately, so the strip is visible while it slides in. */
59
+ visibility: ${props => props.$isExpanded ? 'visible' : 'hidden'};
60
+ transition:
61
+ grid-template-rows ${COLLAPSE_DURATION} ${EASE_OUT},
62
+ visibility 0s linear ${props => props.$isExpanded ? '0s' : COLLAPSE_DURATION};
63
+ ${noMotion}
64
+ `;
65
+ const CollapseContent = styled.div`
66
+ min-height: 0;
67
+ overflow: hidden;
68
+ display: flex;
69
+ flex-direction: column;
70
+ justify-content: flex-end;
71
+ `;
72
+ const Row = styled.div`
73
+ display: flex;
74
+ min-width: 0;
75
+ `;
76
+ const ScrollButton = styled.button.attrs(applyDefaultTheme)`
77
+ flex: none;
78
+ display: flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ width: 24px;
82
+ height: 72px;
83
+ padding: 0;
84
+ border: none;
85
+ background: transparent;
86
+ cursor: pointer;
87
+ ${props => props.theme.themeProp('color', props.theme.getColor('gray-300'), props.theme.getColor('gray-600'))}
88
+
89
+ &:hover:not(:disabled) {
90
+ ${props => props.theme.themeProp('color', props.theme.getColor('gray-100'), props.theme.getColor('gray-900'), 1)}
91
+ }
92
+
93
+ &:disabled {
94
+ opacity: 0.3;
95
+ cursor: default;
96
+ }
97
+
98
+ /* The drop-down triangle, turned to point left or right */
99
+ svg {
100
+ width: 24px;
101
+ height: 24px;
102
+ transform: rotate(${props => props.$direction === 'left' ? '90deg' : '-90deg'});
103
+ }
104
+ `;
105
+ const Filmstrip = styled.div.attrs(applyDefaultTheme)`
106
+ flex: 1;
107
+ min-width: 0;
108
+ display: flex;
109
+ gap: 4px;
110
+ overflow-x: auto;
111
+ /* Thin bar with a transparent track instead of the OS default (white track).
112
+ Kept visible on purpose: mouse users need it to know the strip scrolls. */
113
+ scrollbar-width: thin;
114
+ scrollbar-color: ${props => props.theme.colors.neutral30} transparent;
115
+ /* Room for the bar below the cells. Overlay scrollbars (macOS default) take no space
116
+ and would otherwise sit on top of the thumbnails. */
117
+ padding-bottom: 12px;
118
+ `;
119
+ const spin = keyframes`
120
+ to {
121
+ transform: rotate(360deg);
122
+ }
123
+ `;
124
+ const fadeIn = keyframes`
125
+ from {
126
+ opacity: 0;
127
+ }
128
+ to {
129
+ opacity: 1;
130
+ }
131
+ `;
132
+ const LoadingCell = styled.div`
133
+ flex: none;
134
+ display: flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ width: 96px;
138
+ height: 72px;
139
+ animation: ${fadeIn} 200ms ease-out 300ms backwards;
140
+ `;
141
+ const Spinner = styled.div.attrs(applyDefaultTheme)`
142
+ width: 24px;
143
+ height: 24px;
144
+ border-radius: 50%;
145
+ border: 3px solid;
146
+ ${props => props.theme.themeProp('border-color', `${props.theme.getColor('gray-600')} ${props.theme.getColor('gray-600')} ${props.theme.getColor('gray-600')} ${props.theme.getColor('gray-100')}`, `${props.theme.getColor('gray-300')} ${props.theme.getColor('gray-300')} ${props.theme.getColor('gray-300')} ${props.theme.getColor('gray-600')}`)}
147
+ animation: ${spin} 800ms linear infinite;
148
+
149
+ /* Reduced motion: no spinning; the static ring still shows that something is pending */
150
+ @media (prefers-reduced-motion: reduce) {
151
+ animation: none;
152
+ }
153
+ `;
154
+ const LoadMoreSentinel = styled.div`
155
+ flex: none;
156
+ width: 1px;
157
+ `;
158
+
159
+ export { Collapse, CollapseContent, Filmstrip, LoadMoreSentinel, LoadingCell, Row, ScrollButton, Spinner, ToggleBar, ToggleButton, ToggleIcon, Wrapper };
160
+ //# sourceMappingURL=Filmstrip.styled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Filmstrip.styled.js","sources":["../../../src/components/widgets/Filmstrip/Filmstrip.styled.js"],"sourcesContent":["import styled, { css, keyframes } from 'styled-components';\nimport { applyDefaultTheme } from '../../../utils/defaultTheme';\n\n// One duration and one ease-out curve for both the strip and the arrow, so they move\n// together. Ease-out starts fast and settles gently, which feels responsive.\nconst COLLAPSE_DURATION = '350ms';\nconst EASE_OUT = 'cubic-bezier(0.22, 1, 0.36, 1)';\n\nconst noMotion = css`\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n }\n`;\n\n// Holds the optional toggle bar and the strip, stacked.\nexport const Wrapper = styled.div.attrs(applyDefaultTheme)`\n display: flex;\n flex-direction: column;\n font-family: ${props => props.theme.primaryFontFamily};\n`;\n\n// Full-width bar above the strip; the arrow button sits in the middle.\nexport const ToggleBar = styled.div`\n display: flex;\n justify-content: center;\n`;\n\nexport const ToggleButton = styled.button.attrs(applyDefaultTheme)`\n display: flex;\n align-items: center;\n justify-content: center;\n width: 48px;\n height: 20px;\n padding: 0;\n border: none;\n background: transparent;\n cursor: pointer;\n\n /* The arrow svg uses currentColor, so setting color is enough.\n themeProp(property, dark, light) is the repo's dark mode pattern: it covers the\n body.dark-theme / body.light-theme classes from Storybook's toggle and falls back\n to the system's prefers-color-scheme when neither is set. */\n ${props =>\n props.theme.themeProp('color', props.theme.getColor('gray-300'), props.theme.getColor('gray-600'))}\n\n /* Specificity 1 nests the theme selectors under :hover (same as AssetGalleryGridCard). */\n &:hover {\n ${props =>\n props.theme.themeProp(\n 'color',\n props.theme.getColor('gray-100'),\n props.theme.getColor('gray-900'),\n 1\n )}\n }\n`;\n\n// Rotates the arrow: pointing down while expanded (click to hide),\n// pointing up while collapsed (click to show).\nexport const ToggleIcon = styled.span`\n display: flex;\n transform: rotate(${props => (props.$isExpanded ? '0deg' : '180deg')});\n transition: transform ${COLLAPSE_DURATION} ${EASE_OUT};\n ${noMotion}\n\n svg {\n width: 24px;\n height: 24px;\n }\n`;\n\n// Animates the strip's height. CSS can't transition `height: auto`, but it can\n// transition a grid row between 1fr (full content height) and 0fr (nothing).\nexport const Collapse = styled.div`\n display: grid;\n grid-template-rows: ${props => (props.$isExpanded ? '1fr' : '0fr')};\n /* visibility: hidden removes collapsed cells from the Tab order and screen readers.\n When collapsing it waits until the slide has finished (the delay); when expanding\n it switches back to visible immediately, so the strip is visible while it slides in. */\n visibility: ${props => (props.$isExpanded ? 'visible' : 'hidden')};\n transition:\n grid-template-rows ${COLLAPSE_DURATION} ${EASE_OUT},\n visibility 0s linear ${props => (props.$isExpanded ? '0s' : COLLAPSE_DURATION)};\n ${noMotion}\n`;\n\n// The grid row's only child. min-height: 0 lets it shrink below its content height,\n// and overflow: hidden clips what doesn't fit. Aligning the content to the bottom\n// (flex-end) means the top gets clipped first, so the strip looks like it slides down.\nexport const CollapseContent = styled.div`\n min-height: 0;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n`;\n\n// Puts the optional page buttons on either side of the strip.\nexport const Row = styled.div`\n display: flex;\n min-width: 0;\n`;\n\n// One page button. Same height as a cell (the strip adds 12px of scrollbar room below).\nexport const ScrollButton = styled.button.attrs(applyDefaultTheme)`\n flex: none;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 72px;\n padding: 0;\n border: none;\n background: transparent;\n cursor: pointer;\n ${props =>\n props.theme.themeProp(\n 'color',\n props.theme.getColor('gray-300'),\n props.theme.getColor('gray-600')\n )}\n\n &:hover:not(:disabled) {\n ${props =>\n props.theme.themeProp(\n 'color',\n props.theme.getColor('gray-100'),\n props.theme.getColor('gray-900'),\n 1\n )}\n }\n\n &:disabled {\n opacity: 0.3;\n cursor: default;\n }\n\n /* The drop-down triangle, turned to point left or right */\n svg {\n width: 24px;\n height: 24px;\n transform: rotate(${props => (props.$direction === 'left' ? '90deg' : '-90deg')});\n }\n`;\n\n// Was `styled.p`: a paragraph cannot contain buttons.\n// The outer element scrolls; the cells sit in one row that never wraps.\nexport const Filmstrip = styled.div.attrs(applyDefaultTheme)`\n flex: 1;\n min-width: 0;\n display: flex;\n gap: 4px;\n overflow-x: auto;\n /* Thin bar with a transparent track instead of the OS default (white track).\n Kept visible on purpose: mouse users need it to know the strip scrolls. */\n scrollbar-width: thin;\n scrollbar-color: ${props => props.theme.colors.neutral30} transparent;\n /* Room for the bar below the cells. Overlay scrollbars (macOS default) take no space\n and would otherwise sit on top of the thumbnails. */\n padding-bottom: 12px;\n`;\n\nconst spin = keyframes`\n to {\n transform: rotate(360deg);\n }\n`;\n\nconst fadeIn = keyframes`\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n`;\n\n// Same size as a Cell, so the spinner takes the place where the next thumbnails will appear.\n// The fade-in waits 300ms (animation delay + `backwards`, which applies opacity 0 during the\n// wait). If the page arrives sooner, the cell is removed before anyone sees it.\nexport const LoadingCell = styled.div`\n flex: none;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 96px;\n height: 72px;\n animation: ${fadeIn} 200ms ease-out 300ms backwards;\n`;\n\n// A ring with one colored side, rotating. Colors follow the repo's dark mode pattern.\nexport const Spinner = styled.div.attrs(applyDefaultTheme)`\n width: 24px;\n height: 24px;\n border-radius: 50%;\n border: 3px solid;\n ${props =>\n props.theme.themeProp(\n 'border-color',\n `${props.theme.getColor('gray-600')} ${props.theme.getColor('gray-600')} ${props.theme.getColor('gray-600')} ${props.theme.getColor('gray-100')}`,\n `${props.theme.getColor('gray-300')} ${props.theme.getColor('gray-300')} ${props.theme.getColor('gray-300')} ${props.theme.getColor('gray-600')}`\n )}\n animation: ${spin} 800ms linear infinite;\n\n /* Reduced motion: no spinning; the static ring still shows that something is pending */\n @media (prefers-reduced-motion: reduce) {\n animation: none;\n }\n`;\n\n// Invisible marker after the last cell; see the load-more effect in Filmstrip.js.\n// It needs a width so the browser can tell when it is in view.\nexport const LoadMoreSentinel = styled.div`\n flex: none;\n width: 1px;\n`;\n"],"names":["COLLAPSE_DURATION","EASE_OUT","noMotion","css","Wrapper","styled","div","attrs","applyDefaultTheme","props","theme","primaryFontFamily","ToggleBar","ToggleButton","button","themeProp","getColor","ToggleIcon","span","$isExpanded","Collapse","CollapseContent","Row","ScrollButton","$direction","Filmstrip","colors","neutral30","spin","keyframes","fadeIn","LoadingCell","Spinner","LoadMoreSentinel"],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import React__default from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { ReactComponent as SvgHeadset } from '../../../icons/headset.svg.js';
4
+ import { ReactComponent as SvgPlay } from '../../../icons/play.svg.js';
5
+ import '../../../data/Alert/Alert.js';
6
+ import Badge from '../../../data/Badge/Badge.js';
7
+ import '../../../data/LiveBadge/LiveBadge.js';
8
+ import '../../../data/Popover/Popover.js';
9
+ import '../../../data/SeverityBadge/SeverityBadge.js';
10
+ import '../../../data/Tab/Tab.js';
11
+ import '../../../data/Tabs/Tabs.js';
12
+ import '../../../data/Tooltip/Tooltip.js';
13
+ import '../../../data/VerificationStatusIcon/VerificationStatusIcon.js';
14
+ import { convertMsToHMS } from '../../../utils/dateTime.js';
15
+ import { Placeholder, Image, Thumbnail as Thumbnail$1, BadgePosition } from './Thumbnail.styled.js';
16
+
17
+ const Thumbnail = function Thumbnail({
18
+ thumbnailUrl,
19
+ alt,
20
+ fileType,
21
+ duration
22
+ }) {
23
+ const type = fileType?.toLowerCase();
24
+ const isAudio = type === 'audio';
25
+ const isVideo = type === 'video';
26
+ let preview;
27
+ if (isAudio) {
28
+ preview = React__default.createElement(Placeholder, {
29
+ "aria-hidden": "true"
30
+ }, React__default.createElement(SvgHeadset, {
31
+ "aria-hidden": "true"
32
+ }));
33
+ } else if (thumbnailUrl) {
34
+ preview = React__default.createElement(Image, {
35
+ src: thumbnailUrl,
36
+ alt: alt,
37
+ loading: "lazy"
38
+ });
39
+ } else {
40
+ preview = React__default.createElement(Placeholder, {
41
+ "aria-hidden": "true"
42
+ });
43
+ }
44
+ return React__default.createElement(Thumbnail$1, null, preview, isVideo && React__default.createElement(BadgePosition, null, React__default.createElement(Badge, {
45
+ badgeIcon: React__default.createElement(SvgPlay, null),
46
+ badgeContent: duration ? convertMsToHMS(duration) : null,
47
+ backgroundColors: ['black', 'black'],
48
+ colors: ['#f7f8f9', '#f7f8f9'],
49
+ fontSize: 12,
50
+ fontWeight: 500,
51
+ lineHeight: 16,
52
+ verticalPadding: 4,
53
+ horizontalPadding: 8,
54
+ minWidth: 32,
55
+ height: 24
56
+ })));
57
+ };
58
+ Thumbnail.propTypes = process.env.NODE_ENV !== "production" ? {
59
+ thumbnailUrl: PropTypes.string,
60
+ alt: PropTypes.string,
61
+ fileType: PropTypes.oneOf(['image', 'video', 'pdf', 'file', 'audio']),
62
+ duration: PropTypes.number
63
+ } : {};
64
+
65
+ export { Thumbnail as default };
66
+ //# sourceMappingURL=Thumbnail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Thumbnail.js","sources":["../../../../src/components/widgets/Filmstrip/Thumbnail/Thumbnail.js"],"sourcesContent":["import React from 'react';\nimport PropTypes from 'prop-types';\nimport { HeadsetIcon, PlayIcon } from '../../../../icons';\nimport { Badge } from '../../../data';\nimport { convertMsToHMS } from '../../../../utils/dateTime';\nimport * as S from './Thumbnail.styled';\n\n/**\n * What one Filmstrip cell shows: the image, an audio icon, or a video still with a play pill.\n * This is the content layer. Cell (the slot) and Filmstrip (the list) never look at fileType.\n */\nconst Thumbnail = function Thumbnail({ thumbnailUrl, alt, fileType, duration }) {\n const type = fileType?.toLowerCase();\n const isAudio = type === 'audio';\n const isVideo = type === 'video';\n\n // The placeholders are decoration: the Cell (the option) carries the accessible name\n // through aria-label, so they don't need a role or a name of their own.\n let preview;\n if (isAudio) {\n preview = (\n <S.Placeholder aria-hidden=\"true\">\n <HeadsetIcon aria-hidden=\"true\" />\n </S.Placeholder>\n );\n } else if (thumbnailUrl) {\n preview = <S.Image src={thumbnailUrl} alt={alt} loading=\"lazy\" />;\n } else {\n preview = <S.Placeholder aria-hidden=\"true\" />;\n }\n\n return (\n <S.Thumbnail>\n {preview}\n {isVideo && (\n <S.BadgePosition>\n <Badge\n badgeIcon={<PlayIcon />}\n badgeContent={duration ? convertMsToHMS(duration) : null}\n backgroundColors={['black', 'black']}\n colors={['#f7f8f9', '#f7f8f9']}\n fontSize={12}\n fontWeight={500}\n lineHeight={16}\n verticalPadding={4}\n horizontalPadding={8}\n minWidth={32}\n height={24}\n />\n </S.BadgePosition>\n )}\n </S.Thumbnail>\n );\n};\n\nThumbnail.propTypes = {\n thumbnailUrl: PropTypes.string,\n alt: PropTypes.string,\n fileType: PropTypes.oneOf(['image', 'video', 'pdf', 'file', 'audio']),\n duration: PropTypes.number\n};\n\nexport default Thumbnail;\n"],"names":["Thumbnail","thumbnailUrl","alt","fileType","duration","type","toLowerCase","isAudio","isVideo","preview","React","createElement","S","HeadsetIcon","src","loading","Badge","badgeIcon","PlayIcon","badgeContent","convertMsToHMS","backgroundColors","colors","fontSize","fontWeight","lineHeight","verticalPadding","horizontalPadding","minWidth","height","propTypes","process","env","NODE_ENV","PropTypes","string","oneOf","number"],"mappings":";;;;;;;;;;;;;;;;AAWA,MAAMA,SAAS,GAAG,SAASA,SAASA,CAAC;EAAEC,YAAY;EAAEC,GAAG;EAAEC,QAAQ;AAAEC,EAAAA,QAAAA;AAAS,CAAC,EAAE;AAC9E,EAAA,MAAMC,IAAI,GAAGF,QAAQ,EAAEG,WAAW,EAAE,CAAA;AACpC,EAAA,MAAMC,OAAO,GAAGF,IAAI,KAAK,OAAO,CAAA;AAChC,EAAA,MAAMG,OAAO,GAAGH,IAAI,KAAK,OAAO,CAAA;AAIhC,EAAA,IAAII,OAAO,CAAA;AACX,EAAA,IAAIF,OAAO,EAAE;AACXE,IAAAA,OAAO,GACLC,cAAA,CAAAC,aAAA,CAACC,WAAa,EAAA;MAAC,aAAY,EAAA,MAAA;AAAM,KAAA,EAC/BF,cAAA,CAAAC,aAAA,CAACE,UAAW,EAAA;MAAC,aAAY,EAAA,MAAA;AAAM,KAAE,CACpB,CAChB,CAAA;GACF,MAAM,IAAIZ,YAAY,EAAE;AACvBQ,IAAAA,OAAO,GAAGC,cAAA,CAAAC,aAAA,CAACC,KAAO,EAAA;AAACE,MAAAA,GAAG,EAAEb,YAAa;AAACC,MAAAA,GAAG,EAAEA,GAAI;AAACa,MAAAA,OAAO,EAAC,MAAA;AAAM,KAAE,CAAC,CAAA;AACnE,GAAC,MAAM;AACLN,IAAAA,OAAO,GAAGC,cAAA,CAAAC,aAAA,CAACC,WAAa,EAAA;MAAC,aAAY,EAAA,MAAA;AAAM,KAAE,CAAC,CAAA;AAChD,GAAA;EAEA,OACEF,cAAA,CAAAC,aAAA,CAACC,WAAW,EAAA,IAAA,EACTH,OAAO,EACPD,OAAO,IACNE,cAAA,CAAAC,aAAA,CAACC,aAAe,QACdF,cAAA,CAAAC,aAAA,CAACK,KAAK,EAAA;AACJC,IAAAA,SAAS,EAAEP,cAAA,CAAAC,aAAA,CAACO,OAAQ,MAAE,CAAE;IACxBC,YAAY,EAAEf,QAAQ,GAAGgB,cAAc,CAAChB,QAAQ,CAAC,GAAG,IAAK;AACzDiB,IAAAA,gBAAgB,EAAE,CAAC,OAAO,EAAE,OAAO,CAAE;AACrCC,IAAAA,MAAM,EAAE,CAAC,SAAS,EAAE,SAAS,CAAE;AAC/BC,IAAAA,QAAQ,EAAE,EAAG;AACbC,IAAAA,UAAU,EAAE,GAAI;AAChBC,IAAAA,UAAU,EAAE,EAAG;AACfC,IAAAA,eAAe,EAAE,CAAE;AACnBC,IAAAA,iBAAiB,EAAE,CAAE;AACrBC,IAAAA,QAAQ,EAAE,EAAG;AACbC,IAAAA,MAAM,EAAE,EAAA;GACT,CACc,CAER,CAAC,CAAA;AAElB,EAAC;AAED7B,SAAS,CAAC8B,SAAS,GAAAC,OAAA,CAAAC,GAAA,CAAAC,QAAA,KAAG,YAAA,GAAA;EACpBhC,YAAY,EAAEiC,SAAS,CAACC,MAAM;EAC9BjC,GAAG,EAAEgC,SAAS,CAACC,MAAM;AACrBhC,EAAAA,QAAQ,EAAE+B,SAAS,CAACE,KAAK,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;EACrEhC,QAAQ,EAAE8B,SAAS,CAACG,MAAAA;AACtB,CAAC,GAAA,EAAA;;;;"}
@@ -0,0 +1,37 @@
1
+ import styled from 'styled-components';
2
+ import { applyDefaultTheme } from '../../../utils/defaultTheme.js';
3
+
4
+ const Thumbnail = styled.div`
5
+ position: relative;
6
+ width: 100%;
7
+ height: 100%;
8
+ `;
9
+ const Image = styled.img`
10
+ display: block;
11
+ width: 100%;
12
+ height: 100%;
13
+ object-fit: cover;
14
+ `;
15
+ const Placeholder = styled.div.attrs(applyDefaultTheme)`
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ width: 100%;
20
+ height: 100%;
21
+ background: ${props => props.theme.colors.neutral20};
22
+ color: ${props => props.theme.colors.neutral60};
23
+
24
+ svg {
25
+ width: 24px;
26
+ height: 24px;
27
+ }
28
+ `;
29
+ const BadgePosition = styled.div`
30
+ position: absolute;
31
+ right: 8px;
32
+ bottom: 8px;
33
+ pointer-events: none;
34
+ `;
35
+
36
+ export { BadgePosition, Image, Placeholder, Thumbnail };
37
+ //# sourceMappingURL=Thumbnail.styled.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Thumbnail.styled.js","sources":["../../../../src/components/widgets/Filmstrip/Thumbnail/Thumbnail.styled.js"],"sourcesContent":["import styled from 'styled-components';\nimport { applyDefaultTheme } from '../../../../utils/defaultTheme';\n\n// Fills the cell; `position: relative` is the anchor for the play pill.\nexport const Thumbnail = styled.div`\n position: relative;\n width: 100%;\n height: 100%;\n`;\n\nexport const Image = styled.img`\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n`;\n\nexport const Placeholder = styled.div.attrs(applyDefaultTheme)`\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n background: ${props => props.theme.colors.neutral20};\n color: ${props => props.theme.colors.neutral60};\n\n svg {\n width: 24px;\n height: 24px;\n }\n`;\n\nexport const BadgePosition = styled.div`\n position: absolute;\n right: 8px;\n bottom: 8px;\n pointer-events: none;\n`;\n"],"names":["Thumbnail","styled","div","Image","img","Placeholder","attrs","applyDefaultTheme","props","theme","colors","neutral20","neutral60","BadgePosition"],"mappings":";;;AAIaA,MAAAA,SAAS,GAAGC,MAAM,CAACC,GAAG,CAAA;AACnC;AACA;AACA;AACA,EAAC;AAEYC,MAAAA,KAAK,GAAGF,MAAM,CAACG,GAAG,CAAA;AAC/B;AACA;AACA;AACA;AACA,EAAC;AAEM,MAAMC,WAAW,GAAGJ,MAAM,CAACC,GAAG,CAACI,KAAK,CAACC,iBAAiB,CAAC,CAAA;AAC9D;AACA;AACA;AACA;AACA;AACA,cAAgBC,EAAAA,KAAK,IAAIA,KAAK,CAACC,KAAK,CAACC,MAAM,CAACC,SAAS,CAAA;AACrD,SAAWH,EAAAA,KAAK,IAAIA,KAAK,CAACC,KAAK,CAACC,MAAM,CAACE,SAAS,CAAA;AAChD;AACA;AACA;AACA;AACA;AACA,EAAC;AAEYC,MAAAA,aAAa,GAAGZ,MAAM,CAACC,GAAG,CAAA;AACvC;AACA;AACA;AACA;AACA;;;;"}
@@ -0,0 +1,2 @@
1
+ export { default } from './Filmstrip.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -9,4 +9,5 @@ export { default as AssetAction } from './AssetAction/AssetAction.js';
9
9
  export { default as ProgressBar } from './ProgressBar/ProgressBar.js';
10
10
  export { default as InfoCard } from './InfoCard/InfoCard.js';
11
11
  export { default as InstructionSeverityDisplay } from './InstructionSeverityDisplay/InstructionSeverityDisplay.js';
12
+ export { default as Filmstrip } from './Filmstrip/Filmstrip.js';
12
13
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ntbjs/react-components",
3
- "version": "2.0.18",
3
+ "version": "2.0.20-rc.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -65,10 +65,10 @@
65
65
  "@rollup/plugin-babel": "^5.3.0",
66
66
  "@rollup/plugin-commonjs": "^19.0.0",
67
67
  "@rollup/plugin-node-resolve": "^13.0.0",
68
- "@storybook/addon-docs": "^10.1.11",
69
- "@storybook/addon-links": "^10.1.11",
70
- "@storybook/addon-themes": "^10.1.11",
71
- "@storybook/react-webpack5": "^10.1.11",
68
+ "@storybook/addon-docs": "^10.6.5",
69
+ "@storybook/addon-links": "^10.6.5",
70
+ "@storybook/addon-themes": "^10.6.5",
71
+ "@storybook/react-webpack5": "^10.6.5",
72
72
  "@svgr/rollup": "^5.5.0",
73
73
  "@svgr/webpack": "^5.5.0",
74
74
  "@testing-library/jest-dom": "^5.12.0",
@@ -97,14 +97,13 @@
97
97
  "rollup-plugin-url": "^3.0.1",
98
98
  "sass": "^1.97.2",
99
99
  "sass-loader": "^10.1.1",
100
- "storybook": "^10.1.11",
100
+ "storybook": "^10.6.5",
101
101
  "styled-components": "^6.1.19",
102
102
  "url-loader": "^4.1.1"
103
103
  },
104
104
  "dependencies": {
105
105
  "@floating-ui/react": "^0.26.28",
106
106
  "@restart/hooks": "^0.6.2",
107
- "@storybook/cli": "^10.1.11",
108
107
  "@tippyjs/react": "^4.2.6",
109
108
  "lodash": "^4.17.21",
110
109
  "nanoid": "^3.3.8",