@duro-app/ui 0.47.0 → 0.48.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.
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Table/styles.css.d.ts +29 -0
- package/dist/components/Table/styles.css.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +1310 -1231
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Table/Table.stories.tsx +16 -2
- package/src/components/Table/Table.tsx +52 -1
- package/src/components/Table/styles.css.ts +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.48.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@tanstack/react-virtual": "^3.14.6",
|
|
58
|
-
"@duro-app/tokens": "^0.
|
|
58
|
+
"@duro-app/tokens": "^0.48.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -1198,12 +1198,26 @@ export const ScrollsHorizontallyWhenDense: Story = {
|
|
|
1198
1198
|
play: async ({canvas}) => {
|
|
1199
1199
|
const grid = canvas.getByRole('table')
|
|
1200
1200
|
const scroller = grid.parentElement as HTMLElement
|
|
1201
|
+
const frame = scroller.parentElement as HTMLElement
|
|
1202
|
+
const rightFade = frame.lastElementChild as HTMLElement
|
|
1203
|
+
const leftFade = rightFade.previousElementSibling as HTMLElement
|
|
1201
1204
|
await waitFor(async () => {
|
|
1202
1205
|
// Still tabular: all 8 headers present (NOT carded up).
|
|
1203
1206
|
await expect(canvas.queryAllByRole('columnheader')).toHaveLength(8)
|
|
1207
|
+
// The wrapper scrolls: content (8 × 120px ≈ 960) exceeds its 800px box.
|
|
1208
|
+
await expect(scroller.scrollWidth).toBeGreaterThan(scroller.clientWidth + 2)
|
|
1209
|
+
// Scroll affordance: at the start the RIGHT edge fades (more →), the
|
|
1210
|
+
// left edge doesn't.
|
|
1211
|
+
await expect(getComputedStyle(rightFade).opacity).toBe('1')
|
|
1212
|
+
await expect(getComputedStyle(leftFade).opacity).toBe('0')
|
|
1213
|
+
})
|
|
1214
|
+
// Scroll to the end → the cue flips: the left edge fades, the right clears.
|
|
1215
|
+
scroller.scrollLeft = scroller.scrollWidth
|
|
1216
|
+
scroller.dispatchEvent(new Event('scroll'))
|
|
1217
|
+
await waitFor(async () => {
|
|
1218
|
+
await expect(getComputedStyle(leftFade).opacity).toBe('1')
|
|
1219
|
+
await expect(getComputedStyle(rightFade).opacity).toBe('0')
|
|
1204
1220
|
})
|
|
1205
|
-
// The wrapper scrolls: content (8 × 120px ≈ 960) exceeds its 800px box.
|
|
1206
|
-
await expect(scroller.scrollWidth).toBeGreaterThan(scroller.clientWidth + 2)
|
|
1207
1221
|
},
|
|
1208
1222
|
}
|
|
1209
1223
|
|
|
@@ -4,7 +4,9 @@ import {
|
|
|
4
4
|
type MutableRefObject,
|
|
5
5
|
createContext,
|
|
6
6
|
useContext,
|
|
7
|
+
useEffect,
|
|
7
8
|
useRef,
|
|
9
|
+
useState,
|
|
8
10
|
Children,
|
|
9
11
|
isValidElement,
|
|
10
12
|
} from 'react'
|
|
@@ -169,6 +171,55 @@ function extractColumnMeta(
|
|
|
169
171
|
}
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
// --- ScrollFrame ---
|
|
175
|
+
//
|
|
176
|
+
// Wraps the grid in the horizontal-scroll port and paints a soft fade at
|
|
177
|
+
// whichever edge still has hidden columns, so a scrollable table advertises
|
|
178
|
+
// that there's more to see (and stops advertising once you reach the end).
|
|
179
|
+
// This is the ONLY JS the responsive Table uses, and it's purely cosmetic —
|
|
180
|
+
// it reads scroll position and toggles fade opacity; it never changes layout,
|
|
181
|
+
// so none of the flicker/oscillation risk of the old auto-stacking applies.
|
|
182
|
+
function ScrollFrame({children}: {children: ReactNode}) {
|
|
183
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
184
|
+
const [edges, setEdges] = useState({start: false, end: false})
|
|
185
|
+
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
const el = scrollRef.current
|
|
188
|
+
if (!el) return
|
|
189
|
+
const update = () => {
|
|
190
|
+
const overflowing = el.scrollWidth - el.clientWidth > 1
|
|
191
|
+
setEdges({
|
|
192
|
+
start: overflowing && el.scrollLeft > 1,
|
|
193
|
+
end: overflowing && el.scrollLeft + el.clientWidth < el.scrollWidth - 1,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
update()
|
|
197
|
+
el.addEventListener('scroll', update, {passive: true})
|
|
198
|
+
const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(update) : null
|
|
199
|
+
ro?.observe(el)
|
|
200
|
+
return () => {
|
|
201
|
+
el.removeEventListener('scroll', update)
|
|
202
|
+
ro?.disconnect()
|
|
203
|
+
}
|
|
204
|
+
}, [])
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<html.div style={styles.scrollFrame}>
|
|
208
|
+
<html.div ref={scrollRef} style={styles.scrollX}>
|
|
209
|
+
{children}
|
|
210
|
+
</html.div>
|
|
211
|
+
<html.div
|
|
212
|
+
aria-hidden
|
|
213
|
+
style={[styles.edgeFade, styles.edgeFadeLeft, edges.start && styles.edgeFadeVisible]}
|
|
214
|
+
/>
|
|
215
|
+
<html.div
|
|
216
|
+
aria-hidden
|
|
217
|
+
style={[styles.edgeFade, styles.edgeFadeRight, edges.end && styles.edgeFadeVisible]}
|
|
218
|
+
/>
|
|
219
|
+
</html.div>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
172
223
|
// --- Root ---
|
|
173
224
|
|
|
174
225
|
interface RootProps {
|
|
@@ -240,7 +291,7 @@ export function Root({
|
|
|
240
291
|
// a dense table overflows this wrapper and scrolls sideways instead of
|
|
241
292
|
// crushing. Below it (@container ≤ sm) the grid is one column and there's
|
|
242
293
|
// nothing to scroll. Non-responsive tables opt out of the frame entirely.
|
|
243
|
-
const framedGrid = responsive ? <
|
|
294
|
+
const framedGrid = responsive ? <ScrollFrame>{grid}</ScrollFrame> : grid
|
|
244
295
|
|
|
245
296
|
// Slots render unconditionally — `responsive=false` still wants its sort/
|
|
246
297
|
// pagination chrome. Only the containerType:inline-size wrapper is
|
|
@@ -347,6 +347,42 @@ export const styles = css.create({
|
|
|
347
347
|
WebkitOverflowScrolling: 'touch',
|
|
348
348
|
},
|
|
349
349
|
|
|
350
|
+
// Positioning context for the scroll-affordance edge fades, which sit on
|
|
351
|
+
// top of the scroll port (not inside it, so they don't scroll away).
|
|
352
|
+
scrollFrame: {
|
|
353
|
+
position: 'relative',
|
|
354
|
+
},
|
|
355
|
+
// A soft fade pinned to a scroll edge that hints "there's more this way".
|
|
356
|
+
// It fades the content into the surface colour, so it reads in any theme.
|
|
357
|
+
// Toggled by opacity from JS scroll position; pointer-events off so it
|
|
358
|
+
// never eats clicks. Sits just inside the rounded frame border.
|
|
359
|
+
edgeFade: {
|
|
360
|
+
position: 'absolute',
|
|
361
|
+
top: 1,
|
|
362
|
+
bottom: 1,
|
|
363
|
+
width: spacing.xl,
|
|
364
|
+
pointerEvents: 'none',
|
|
365
|
+
opacity: 0,
|
|
366
|
+
transitionProperty: 'opacity',
|
|
367
|
+
transitionDuration: duration.fast,
|
|
368
|
+
transitionTimingFunction: easing.standard,
|
|
369
|
+
},
|
|
370
|
+
edgeFadeLeft: {
|
|
371
|
+
left: 1,
|
|
372
|
+
borderTopLeftRadius: radii.md,
|
|
373
|
+
borderBottomLeftRadius: radii.md,
|
|
374
|
+
backgroundImage: `linear-gradient(to left, transparent, ${colors.bgCard})`,
|
|
375
|
+
},
|
|
376
|
+
edgeFadeRight: {
|
|
377
|
+
right: 1,
|
|
378
|
+
borderTopRightRadius: radii.md,
|
|
379
|
+
borderBottomRightRadius: radii.md,
|
|
380
|
+
backgroundImage: `linear-gradient(to right, transparent, ${colors.bgCard})`,
|
|
381
|
+
},
|
|
382
|
+
edgeFadeVisible: {
|
|
383
|
+
opacity: 1,
|
|
384
|
+
},
|
|
385
|
+
|
|
350
386
|
// Dynamic: grid columns applied on Root (non-responsive path only).
|
|
351
387
|
gridColumns: (template: string) => ({
|
|
352
388
|
gridTemplateColumns: template,
|