@duro-app/ui 5.9.0 → 5.11.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.
Files changed (32) hide show
  1. package/dist/{Table-CAjPFgTX.js → Table-ColfpgKV.js} +306 -300
  2. package/dist/Table-ColfpgKV.js.map +1 -0
  3. package/dist/components/FileTrigger/FileTrigger.d.ts +23 -0
  4. package/dist/components/FileTrigger/FileTrigger.d.ts.map +1 -0
  5. package/dist/components/FileTrigger/FileTrigger.meta.d.ts +3 -0
  6. package/dist/components/FileTrigger/FileTrigger.meta.d.ts.map +1 -0
  7. package/dist/components/FileTrigger/FileTrigger.stories.d.ts +9 -0
  8. package/dist/components/FileTrigger/FileTrigger.stories.d.ts.map +1 -0
  9. package/dist/components/FileTrigger/index.d.ts +3 -0
  10. package/dist/components/FileTrigger/index.d.ts.map +1 -0
  11. package/dist/components/FileTrigger/pick.d.ts +14 -0
  12. package/dist/components/FileTrigger/pick.d.ts.map +1 -0
  13. package/dist/components/Icon/Icon.d.ts +1 -1
  14. package/dist/components/Icon/Icon.d.ts.map +1 -1
  15. package/dist/components/Icon/Icon.stories.d.ts +8 -0
  16. package/dist/components/Icon/Icon.stories.d.ts.map +1 -0
  17. package/dist/index.css +1 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1940 -1859
  21. package/dist/index.js.map +1 -1
  22. package/dist/table.js +1 -1
  23. package/package.json +2 -2
  24. package/src/components/FileTrigger/FileTrigger.meta.ts +24 -0
  25. package/src/components/FileTrigger/FileTrigger.stories.tsx +36 -0
  26. package/src/components/FileTrigger/FileTrigger.tsx +80 -0
  27. package/src/components/FileTrigger/index.ts +2 -0
  28. package/src/components/FileTrigger/pick.ts +21 -0
  29. package/src/components/Icon/Icon.stories.tsx +112 -0
  30. package/src/components/Icon/Icon.tsx +50 -0
  31. package/src/index.ts +5 -0
  32. package/dist/Table-CAjPFgTX.js.map +0 -1
package/dist/table.js CHANGED
@@ -1,4 +1,4 @@
1
- import { M as F, m as xt, R as rt, H as kt, n as pt, o as bt, p as ft, q as mt, b as ht, t as yt, r as St, v as Tt } from "./Table-CAjPFgTX.js";
1
+ import { M as F, m as xt, R as rt, H as kt, n as pt, o as bt, p as ft, q as mt, b as ht, t as yt, r as St, v as Tt } from "./Table-ColfpgKV.js";
2
2
  import { jsx as u, jsxs as q } from "react/jsx-runtime";
3
3
  import { c as H, m as p, d as b } from "./compiler-runtime-BuugS4D0.js";
4
4
  import { flexRender as E, useReactTable as it, getPaginationRowModel as Ct, getFilteredRowModel as vt, getSortedRowModel as at, getCoreRowModel as st } from "@tanstack/react-table";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "5.9.0",
3
+ "version": "5.11.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@tanstack/react-virtual": "^3.14.6",
69
- "@duro-app/tokens": "^5.9.0"
69
+ "@duro-app/tokens": "^5.11.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@babel/preset-typescript": "^7.28.0",
@@ -0,0 +1,24 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'A Button that opens the OS file picker. Pass the chosen files to onSelect; call open() on its ref to open the picker from elsewhere.',
6
+ whenToUse: [
7
+ 'Uploading or attaching one or more files',
8
+ 'Opening the file picker from a custom control via the ref',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Drag-and-drop zones — build on a drop target instead',
12
+ 'Triggering a non-file action — use Button',
13
+ ],
14
+ relatedTo: [
15
+ {
16
+ component: 'Button',
17
+ kind: 'composition',
18
+ relationship: 'FileTrigger renders a Button and forwards variant, size and disabled',
19
+ },
20
+ ],
21
+ example: `<FileTrigger accept="image/*" multiple onSelect={(files) => upload(files)}>
22
+ Choose images
23
+ </FileTrigger>`,
24
+ }
@@ -0,0 +1,36 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect, fn} from 'storybook/test'
3
+ import {FileTrigger} from './FileTrigger'
4
+
5
+ const meta: Meta<typeof FileTrigger> = {
6
+ title: 'Components/FileTrigger',
7
+ component: FileTrigger,
8
+ args: {onSelect: fn()},
9
+ argTypes: {
10
+ variant: {control: 'select', options: ['primary', 'secondary', 'link', 'danger']},
11
+ size: {control: 'select', options: ['default', 'small']},
12
+ multiple: {control: 'boolean'},
13
+ disabled: {control: 'boolean'},
14
+ },
15
+ }
16
+
17
+ export default meta
18
+ type Story = StoryObj<typeof FileTrigger>
19
+
20
+ export const Default: Story = {
21
+ args: {children: 'Choose file', variant: 'secondary'},
22
+ play: async ({args, canvas, userEvent, canvasElement}) => {
23
+ await expect(canvas.getByRole('button', {name: 'Choose file'})).toBeEnabled()
24
+ const input = canvasElement.querySelector<HTMLInputElement>('input[type=file]')!
25
+ await userEvent.upload(input, new File(['hi'], 'hi.txt', {type: 'text/plain'}))
26
+ await expect(args.onSelect).toHaveBeenCalledTimes(1)
27
+ },
28
+ }
29
+
30
+ export const Images: Story = {
31
+ args: {children: 'Choose images', accept: 'image/*', multiple: true},
32
+ }
33
+
34
+ export const Disabled: Story = {
35
+ args: {children: 'Choose file', disabled: true},
36
+ }
@@ -0,0 +1,80 @@
1
+ import {useEffect, useImperativeHandle, useRef} from 'react'
2
+ import type {ReactNode, Ref} from 'react'
3
+ import {css, html} from 'react-strict-dom'
4
+ import {Button, type ButtonSize, type ButtonVariant} from '../Button/Button'
5
+ import {openPicker, takeFiles} from './pick'
6
+
7
+ export interface FileTriggerHandle {
8
+ /** Open the OS file picker programmatically. */
9
+ open: () => void
10
+ }
11
+
12
+ export interface FileTriggerProps {
13
+ /** Accepted types, as the native `accept` attribute (e.g. `image/*,.pdf`). */
14
+ accept?: string
15
+ multiple?: boolean
16
+ /** Called with the chosen files. */
17
+ onSelect: (files: File[]) => void
18
+ variant?: ButtonVariant
19
+ size?: ButtonSize
20
+ disabled?: boolean
21
+ 'aria-label'?: string
22
+ /** Exposes `open()` so a caller can open the picker without the button. */
23
+ ref?: Ref<FileTriggerHandle>
24
+ children: ReactNode
25
+ }
26
+
27
+ // display:none, not visually hidden: the input is only ever opened by click(),
28
+ // which a hidden input still honours, and this keeps its "No file chosen" text
29
+ // out of the accessibility tree (axe flagged its contrast).
30
+ const styles = css.create({
31
+ input: {display: 'none'},
32
+ })
33
+
34
+ /** A Button that opens the OS file picker. Web only: native has no file input. */
35
+ export function FileTrigger({
36
+ accept,
37
+ multiple = false,
38
+ onSelect,
39
+ variant,
40
+ size,
41
+ disabled,
42
+ 'aria-label': ariaLabel,
43
+ ref,
44
+ children,
45
+ }: FileTriggerProps) {
46
+ const inputRef = useRef<HTMLInputElement>(null)
47
+ useImperativeHandle(ref, () => ({open: () => openPicker(inputRef.current)}), [])
48
+
49
+ // RSD's input props omit `accept`, so set the DOM attribute directly.
50
+ useEffect(() => {
51
+ const input = inputRef.current
52
+ if (!input) return
53
+ if (accept) input.setAttribute('accept', accept)
54
+ else input.removeAttribute('accept')
55
+ }, [accept])
56
+
57
+ return (
58
+ <>
59
+ <Button
60
+ variant={variant}
61
+ size={size}
62
+ disabled={disabled}
63
+ aria-label={ariaLabel}
64
+ onClick={() => openPicker(inputRef.current)}
65
+ >
66
+ {children}
67
+ </Button>
68
+ <html.input
69
+ ref={inputRef}
70
+ type="file"
71
+ multiple={multiple}
72
+ disabled={disabled}
73
+ tabIndex={-1}
74
+ aria-hidden={true}
75
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => takeFiles(e.target, onSelect)}
76
+ style={styles.input}
77
+ />
78
+ </>
79
+ )
80
+ }
@@ -0,0 +1,2 @@
1
+ export {FileTrigger} from './FileTrigger'
2
+ export type {FileTriggerProps, FileTriggerHandle} from './FileTrigger'
@@ -0,0 +1,21 @@
1
+ /** The slice of an <input type=file> the picker logic touches. */
2
+ export interface FileInputLike {
3
+ files: ArrayLike<File> | null
4
+ value: string
5
+ click: () => void
6
+ }
7
+
8
+ /** Open the native file picker. */
9
+ export function openPicker(input: Pick<FileInputLike, 'click'> | null) {
10
+ input?.click()
11
+ }
12
+
13
+ /**
14
+ * Hand the chosen files to `onSelect`, then reset the input so picking the
15
+ * same file again still fires a change event.
16
+ */
17
+ export function takeFiles(input: FileInputLike, onSelect: (files: File[]) => void) {
18
+ const files = input.files ? Array.from(input.files) : []
19
+ input.value = ''
20
+ if (files.length > 0) onSelect(files)
21
+ }
@@ -0,0 +1,112 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {css, html} from 'react-strict-dom'
3
+ import {Icon} from './Icon'
4
+ import type {IconName} from './Icon'
5
+ import {spacing} from '@duro-app/tokens/tokens/spacing.css'
6
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
7
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
8
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
9
+
10
+ const meta: Meta<typeof Icon> = {
11
+ title: 'Components/Icon',
12
+ component: Icon,
13
+ argTypes: {
14
+ size: {control: 'select', options: ['sm', 'md', 'lg', 'xl', 'xxl']},
15
+ },
16
+ }
17
+
18
+ export default meta
19
+ type Story = StoryObj<typeof Icon>
20
+
21
+ // The groups of the IconName union, in its order.
22
+ const groups: ReadonlyArray<{title: string; names: readonly IconName[]}> = [
23
+ {
24
+ title: 'Core',
25
+ names: [
26
+ 'x-circle',
27
+ 'check-circle',
28
+ 'check-done',
29
+ 'clock',
30
+ 'forbidden',
31
+ 'info-circle',
32
+ 'alert-triangle',
33
+ 'shield',
34
+ 'lock',
35
+ 'key',
36
+ ],
37
+ },
38
+ {
39
+ title: 'Navigation / wayfinding',
40
+ names: [
41
+ 'map',
42
+ 'layers',
43
+ 'repeat',
44
+ 'database',
45
+ 'shield-check',
46
+ 'route',
47
+ 'git-branch',
48
+ 'menu',
49
+ 'pin',
50
+ ],
51
+ },
52
+ {
53
+ title: 'Infrastructure / inventory',
54
+ names: ['server', 'hard-drive', 'box', 'image', 'tag', 'pie-chart'],
55
+ },
56
+ {title: 'People / access / admin', names: ['users', 'user-plus', 'mail', 'file-text', 'plug']},
57
+ {title: 'Input / action', names: ['search', 'mic']},
58
+ {title: 'Color mode', names: ['sun', 'moon', 'monitor', 'contrast']},
59
+ {title: 'Device status', names: ['signal', 'battery']},
60
+ {title: 'Media', names: ['video', 'music', 'play', 'pause']},
61
+ {
62
+ title: 'Filled variants',
63
+ names: [
64
+ 'info-circle-filled',
65
+ 'alert-triangle-filled',
66
+ 'check-circle-filled',
67
+ 'x-circle-filled',
68
+ 'shield-filled',
69
+ 'lock-filled',
70
+ ],
71
+ },
72
+ ]
73
+
74
+ const styles = css.create({
75
+ page: {display: 'flex', flexDirection: 'column', gap: spacing.xl, color: colors.text},
76
+ heading: {
77
+ fontSize: typography.fontSizeSm,
78
+ fontWeight: typography.fontWeightSemibold,
79
+ color: colors.textMuted,
80
+ },
81
+ grid: {
82
+ display: 'grid',
83
+ gridTemplateColumns: `repeat(auto-fill, minmax(${sizes.gridColSm}, 1fr))`,
84
+ gap: spacing.lg,
85
+ },
86
+ cell: {display: 'flex', flexDirection: 'column', alignItems: 'center', gap: spacing.sm},
87
+ label: {fontSize: typography.fontSizeSm, color: colors.textMuted},
88
+ })
89
+
90
+ export const Default: Story = {
91
+ args: {name: 'signal', size: 'lg'},
92
+ }
93
+
94
+ export const AllIcons: Story = {
95
+ render: () => (
96
+ <html.div style={styles.page}>
97
+ {groups.map((group) => (
98
+ <html.section key={group.title}>
99
+ <html.h3 style={styles.heading}>{group.title}</html.h3>
100
+ <html.div style={styles.grid}>
101
+ {group.names.map((name) => (
102
+ <html.div key={name} style={styles.cell}>
103
+ <Icon name={name} size="lg" />
104
+ <html.span style={styles.label}>{name}</html.span>
105
+ </html.div>
106
+ ))}
107
+ </html.div>
108
+ </html.section>
109
+ ))}
110
+ </html.div>
111
+ ),
112
+ }
@@ -45,6 +45,14 @@ export type IconName =
45
45
  | 'moon'
46
46
  | 'monitor'
47
47
  | 'contrast'
48
+ // Device status glyphs
49
+ | 'signal'
50
+ | 'battery'
51
+ // Media glyphs
52
+ | 'video'
53
+ | 'music'
54
+ | 'play'
55
+ | 'pause'
48
56
  // Filled variants (solid shape with cutout symbol)
49
57
  | 'info-circle-filled'
50
58
  | 'alert-triangle-filled'
@@ -313,6 +321,48 @@ const strokeIcons: Partial<Record<IconName, ReactNode>> = {
313
321
  <path d="M12 2a10 10 0 0 1 0 20z" fill="currentColor" stroke="none" />
314
322
  </>
315
323
  ),
324
+ // ---- device status ----
325
+ // Four ascending bars — cellular signal strength.
326
+ signal: (
327
+ <>
328
+ <line x1="4" y1="20" x2="4" y2="16" />
329
+ <line x1="9" y1="20" x2="9" y2="12" />
330
+ <line x1="14" y1="20" x2="14" y2="8" />
331
+ <line x1="19" y1="20" x2="19" y2="4" />
332
+ </>
333
+ ),
334
+ // Horizontal battery outline with a terminal nub — charge level.
335
+ battery: (
336
+ <>
337
+ <rect x="2" y="7" width="17" height="10" rx="2" />
338
+ <line x1="22" y1="11" x2="22" y2="13" />
339
+ </>
340
+ ),
341
+ // ---- media ----
342
+ // Camera body with a side lens — video file or clip.
343
+ video: (
344
+ <>
345
+ <rect x="2" y="6" width="14" height="12" rx="2" />
346
+ <path d="M16 10l6-3v10l-6-3z" />
347
+ </>
348
+ ),
349
+ // Beamed note — audio file or track.
350
+ music: (
351
+ <>
352
+ <path d="M9 18V5l12-2v13" />
353
+ <circle cx="6" cy="18" r="3" />
354
+ <circle cx="18" cy="16" r="3" />
355
+ </>
356
+ ),
357
+ // Right-pointing triangle — start playback.
358
+ play: <path d="M6 4l14 8-14 8z" />,
359
+ // Two vertical bars — pause playback.
360
+ pause: (
361
+ <>
362
+ <rect x="6" y="4" width="4" height="16" />
363
+ <rect x="14" y="4" width="4" height="16" />
364
+ </>
365
+ ),
316
366
  }
317
367
 
318
368
  /**
package/src/index.ts CHANGED
@@ -20,6 +20,11 @@ export {Icon, type IconName} from './components/Icon'
20
20
  export {Badge, type BadgeVariant, type BadgeSize} from './components/Badge/Badge'
21
21
  export {Breadcrumb} from './components/Breadcrumb/Breadcrumb'
22
22
  export {Button, type ButtonVariant, type ButtonSize} from './components/Button/Button'
23
+ export {
24
+ FileTrigger,
25
+ type FileTriggerProps,
26
+ type FileTriggerHandle,
27
+ } from './components/FileTrigger/FileTrigger'
23
28
  export type {ControlSize} from './shared/types'
24
29
  export {ButtonGroup, type ButtonGroupProps} from './components/ButtonGroup/ButtonGroup'
25
30
  export {Callout, type CalloutVariant} from './components/Callout/Callout'