@duro-app/ui 5.10.0 → 5.12.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/{Table-gRiLI243.js → Table-ColfpgKV.js} +8 -4
- package/dist/Table-ColfpgKV.js.map +1 -0
- package/dist/compiler-runtime-BuugS4D0.js.map +1 -1
- package/dist/components/Alert/Alert.d.ts.map +1 -1
- package/dist/components/Button/Button.d.ts +2 -1
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/Button/styles.css.d.ts +1 -0
- package/dist/components/Button/styles.css.d.ts.map +1 -1
- package/dist/components/Callout/Callout.d.ts.map +1 -1
- package/dist/components/Callout/styles.css.d.ts +3 -0
- package/dist/components/Callout/styles.css.d.ts.map +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.d.ts.map +1 -1
- package/dist/components/DetailPanel/DetailPanel.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.d.ts +2 -17
- package/dist/components/Dialog/Dialog.d.ts.map +1 -1
- package/dist/components/Dialog/Dialog.stories.d.ts +4 -0
- package/dist/components/Dialog/Dialog.stories.d.ts.map +1 -1
- package/dist/components/Dialog/native.css.d.ts +82 -0
- package/dist/components/Dialog/native.css.d.ts.map +1 -0
- package/dist/components/Dialog/parts.css.d.ts +82 -0
- package/dist/components/Dialog/parts.css.d.ts.map +1 -0
- package/dist/components/Dialog/parts.d.ts +35 -0
- package/dist/components/Dialog/parts.d.ts.map +1 -0
- package/dist/components/Dialog/styles.css.d.ts +1 -74
- package/dist/components/Dialog/styles.css.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/Drawer/styles.css.d.ts +2 -0
- package/dist/components/Drawer/styles.css.d.ts.map +1 -1
- package/dist/components/FileTrigger/FileTrigger.d.ts +23 -0
- package/dist/components/FileTrigger/FileTrigger.d.ts.map +1 -0
- package/dist/components/FileTrigger/FileTrigger.meta.d.ts +3 -0
- package/dist/components/FileTrigger/FileTrigger.meta.d.ts.map +1 -0
- package/dist/components/FileTrigger/FileTrigger.stories.d.ts +9 -0
- package/dist/components/FileTrigger/FileTrigger.stories.d.ts.map +1 -0
- package/dist/components/FileTrigger/index.d.ts +3 -0
- package/dist/components/FileTrigger/index.d.ts.map +1 -0
- package/dist/components/FileTrigger/pick.d.ts +14 -0
- package/dist/components/FileTrigger/pick.d.ts.map +1 -0
- package/dist/components/Grid/styles.css.d.ts +1 -0
- package/dist/components/Grid/styles.css.d.ts.map +1 -1
- package/dist/components/Icon/Icon.d.ts +3 -9
- package/dist/components/Icon/Icon.d.ts.map +1 -1
- package/dist/components/Icon/Icon.stories.d.ts.map +1 -1
- package/dist/components/Icon/geometry.d.ts +60 -0
- package/dist/components/Icon/geometry.d.ts.map +1 -0
- package/dist/components/Icon/types.d.ts +15 -0
- package/dist/components/Icon/types.d.ts.map +1 -0
- package/dist/components/Input/Input.d.ts +5 -1
- package/dist/components/Input/Input.d.ts.map +1 -1
- package/dist/components/Input/styles.css.d.ts +1 -0
- package/dist/components/Input/styles.css.d.ts.map +1 -1
- package/dist/components/LinkButton/styles.css.d.ts +1 -0
- package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
- package/dist/components/Meter/styles.css.d.ts +1 -0
- package/dist/components/Meter/styles.css.d.ts.map +1 -1
- package/dist/components/PageShell/styles.css.d.ts +2 -0
- package/dist/components/PageShell/styles.css.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts +1 -17
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.stories.d.ts +3 -0
- package/dist/components/RadioGroup/RadioGroup.stories.d.ts.map +1 -1
- package/dist/components/RadioGroup/group.d.ts +31 -0
- package/dist/components/RadioGroup/group.d.ts.map +1 -0
- package/dist/components/RadioGroup/styles.css.d.ts +10 -0
- package/dist/components/RadioGroup/styles.css.d.ts.map +1 -1
- package/dist/components/ScrollArea/styles.css.d.ts +1 -0
- package/dist/components/ScrollArea/styles.css.d.ts.map +1 -1
- package/dist/components/Spinner/Spinner.d.ts +2 -6
- package/dist/components/Spinner/Spinner.d.ts.map +1 -1
- package/dist/components/Spinner/Spinner.types.d.ts +6 -0
- package/dist/components/Spinner/Spinner.types.d.ts.map +1 -0
- package/dist/components/Spinner/ring.css.d.ts +39 -0
- package/dist/components/Spinner/ring.css.d.ts.map +1 -0
- package/dist/components/Spinner/styles.css.d.ts +1 -28
- package/dist/components/Spinner/styles.css.d.ts.map +1 -1
- package/dist/components/StatusIcon/StatusIcon.d.ts.map +1 -1
- package/dist/components/StatusIcon/styles.css.d.ts +3 -0
- package/dist/components/StatusIcon/styles.css.d.ts.map +1 -1
- package/dist/components/Switch/Switch.d.ts +2 -18
- package/dist/components/Switch/Switch.d.ts.map +1 -1
- package/dist/components/Switch/Switch.types.d.ts +18 -0
- package/dist/components/Switch/Switch.types.d.ts.map +1 -0
- package/dist/components/TableOfContents/styles.css.d.ts +1 -0
- package/dist/components/TableOfContents/styles.css.d.ts.map +1 -1
- package/dist/components/Textarea/styles.css.d.ts +1 -0
- package/dist/components/Textarea/styles.css.d.ts.map +1 -1
- package/dist/components/Toggle/Toggle.d.ts.map +1 -1
- package/dist/components/Toggle/styles.css.d.ts +11 -2
- package/dist/components/Toggle/styles.css.d.ts.map +1 -1
- package/dist/components/VirtualTable/styles.css.d.ts +6 -0
- package/dist/components/VirtualTable/styles.css.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4589 -3788
- package/dist/index.js.map +1 -1
- package/dist/shared/portal/hostRegistry.d.ts +33 -0
- package/dist/shared/portal/hostRegistry.d.ts.map +1 -0
- package/dist/shared/useOverlayOpenState.d.ts +40 -0
- package/dist/shared/useOverlayOpenState.d.ts.map +1 -0
- package/dist/table.js +141 -135
- package/dist/table.js.map +1 -1
- package/package.json +34 -4
- package/src/components/Alert/Alert.tsx +14 -2
- package/src/components/Button/Button.tsx +3 -2
- package/src/components/Button/styles.css.ts +2 -0
- package/src/components/Callout/Callout.tsx +17 -3
- package/src/components/Callout/styles.css.ts +5 -0
- package/src/components/Checkbox/Checkbox.native.tsx +67 -0
- package/src/components/Checkbox/Checkbox.tsx +1 -50
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +3 -0
- package/src/components/DetailPanel/DetailPanel.tsx +9 -45
- package/src/components/Dialog/Dialog.native.tsx +296 -0
- package/src/components/Dialog/Dialog.stories.tsx +71 -0
- package/src/components/Dialog/Dialog.tsx +41 -130
- package/src/components/Dialog/native.css.ts +78 -0
- package/src/components/Dialog/parts.css.ts +101 -0
- package/src/components/Dialog/parts.tsx +61 -0
- package/src/components/Dialog/styles.css.ts +2 -89
- package/src/components/Drawer/Drawer.tsx +20 -49
- package/src/components/Drawer/styles.css.ts +4 -0
- package/src/components/FileTrigger/FileTrigger.meta.ts +24 -0
- package/src/components/FileTrigger/FileTrigger.stories.tsx +36 -0
- package/src/components/FileTrigger/FileTrigger.tsx +80 -0
- package/src/components/FileTrigger/index.ts +2 -0
- package/src/components/FileTrigger/pick.ts +21 -0
- package/src/components/Grid/styles.css.ts +2 -0
- package/src/components/Icon/Icon.native.tsx +54 -0
- package/src/components/Icon/Icon.stories.tsx +1 -0
- package/src/components/Icon/Icon.tsx +19 -400
- package/src/components/Icon/geometry.ts +456 -0
- package/src/components/Icon/types.ts +74 -0
- package/src/components/Input/Input.tsx +8 -0
- package/src/components/Input/styles.css.ts +2 -0
- package/src/components/LinkButton/styles.css.ts +2 -0
- package/src/components/Meter/styles.css.ts +2 -0
- package/src/components/PageShell/styles.css.ts +4 -0
- package/src/components/RadioGroup/RadioGroup.meta.ts +1 -1
- package/src/components/RadioGroup/RadioGroup.native.tsx +48 -0
- package/src/components/RadioGroup/RadioGroup.stories.tsx +64 -1
- package/src/components/RadioGroup/RadioGroup.tsx +8 -83
- package/src/components/RadioGroup/group.tsx +85 -0
- package/src/components/RadioGroup/styles.css.ts +13 -0
- package/src/components/ScrollArea/styles.css.ts +2 -0
- package/src/components/Spinner/Spinner.native.tsx +37 -0
- package/src/components/Spinner/Spinner.tsx +6 -15
- package/src/components/Spinner/Spinner.types.ts +6 -0
- package/src/components/Spinner/ring.css.ts +45 -0
- package/src/components/Spinner/styles.css.ts +2 -32
- package/src/components/StatusIcon/StatusIcon.tsx +13 -2
- package/src/components/StatusIcon/styles.css.ts +5 -0
- package/src/components/Switch/Switch.native.tsx +45 -0
- package/src/components/Switch/Switch.tsx +22 -44
- package/src/components/Switch/Switch.types.ts +18 -0
- package/src/components/TableOfContents/styles.css.ts +2 -0
- package/src/components/Textarea/styles.css.ts +2 -0
- package/src/components/ThemeProvider/ThemeProvider.native.tsx +70 -10
- package/src/components/Toggle/Toggle.tsx +2 -0
- package/src/components/Toggle/styles.css.ts +14 -2
- package/src/components/VirtualTable/styles.css.ts +12 -0
- package/src/index.native.ts +28 -1
- package/src/index.ts +5 -0
- package/src/native-guard/globals.d.ts +10 -0
- package/src/native-guard/react-dom.d.ts +3 -0
- package/src/shared/accessibilityFocus.native.ts +23 -0
- package/src/shared/portal/OverlayModal.native.tsx +51 -0
- package/src/shared/portal/PortalHost.native.tsx +102 -0
- package/src/shared/portal/hostRegistry.ts +76 -0
- package/src/shared/portal/themeScope.native.ts +18 -0
- package/src/shared/safeArea.native.ts +22 -0
- package/src/shared/useOverlayOpenState.ts +128 -0
- package/dist/Table-gRiLI243.js.map +0 -1
- package/dist/components/ThemeProvider/ThemeProvider.native.d.ts +0 -10
- package/dist/components/ThemeProvider/ThemeProvider.native.d.ts.map +0 -1
- package/dist/index.native.d.ts +0 -33
- package/dist/index.native.d.ts.map +0 -1
- package/dist/platform.native.d.ts +0 -2
- package/dist/platform.native.d.ts.map +0 -1
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
useRef,
|
|
12
12
|
} from 'react'
|
|
13
13
|
import {html} from 'react-strict-dom'
|
|
14
|
-
import
|
|
14
|
+
import type {DurationToken} from '@duro-app/tokens/keys'
|
|
15
15
|
import {styles} from './styles.css'
|
|
16
|
+
import {useOverlayOpenState} from '../../shared/useOverlayOpenState'
|
|
16
17
|
import {usePortalMount} from '../ThemeProvider/ThemeProvider'
|
|
17
18
|
import {ControlContextBoundary} from '../Toolbar/ControlContextBoundary'
|
|
18
19
|
import {ModalContext} from '../../shared/ModalContext'
|
|
@@ -89,57 +90,27 @@ function Root({
|
|
|
89
90
|
swipeDismiss = true,
|
|
90
91
|
closeAnimationDuration = 'fast',
|
|
91
92
|
}: RootProps) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
const {
|
|
94
|
+
present,
|
|
95
|
+
closing,
|
|
96
|
+
requestOpen,
|
|
97
|
+
requestClose,
|
|
98
|
+
closeNow: requestCloseImmediate,
|
|
99
|
+
} = useOverlayOpenState({
|
|
100
|
+
open: controlledOpen,
|
|
101
|
+
defaultOpen,
|
|
102
|
+
onOpenChange,
|
|
103
|
+
exitDuration: closeAnimationDuration,
|
|
104
|
+
closeNotice: 'after-exit',
|
|
105
|
+
})
|
|
96
106
|
const panelRef = useRef<HTMLDivElement>(null)
|
|
97
107
|
|
|
98
|
-
const isOpen = isControlled ? controlledOpen : internalOpen
|
|
99
|
-
|
|
100
108
|
const titleId = useId()
|
|
101
109
|
const descriptionId = useId()
|
|
102
110
|
|
|
103
|
-
const requestOpen = useCallback(() => {
|
|
104
|
-
if (closingTimerRef.current) {
|
|
105
|
-
clearTimeout(closingTimerRef.current)
|
|
106
|
-
closingTimerRef.current = null
|
|
107
|
-
}
|
|
108
|
-
setClosing(false)
|
|
109
|
-
if (!isControlled) setInternalOpen(true)
|
|
110
|
-
onOpenChange?.(true)
|
|
111
|
-
}, [isControlled, onOpenChange])
|
|
112
|
-
|
|
113
|
-
const requestClose = useCallback(() => {
|
|
114
|
-
setClosing(true)
|
|
115
|
-
closingTimerRef.current = setTimeout(() => {
|
|
116
|
-
setClosing(false)
|
|
117
|
-
closingTimerRef.current = null
|
|
118
|
-
if (!isControlled) setInternalOpen(false)
|
|
119
|
-
onOpenChange?.(false)
|
|
120
|
-
}, DURATION_MS[closeAnimationDuration])
|
|
121
|
-
}, [isControlled, onOpenChange, closeAnimationDuration])
|
|
122
|
-
|
|
123
|
-
const requestCloseImmediate = useCallback(() => {
|
|
124
|
-
if (closingTimerRef.current) {
|
|
125
|
-
clearTimeout(closingTimerRef.current)
|
|
126
|
-
closingTimerRef.current = null
|
|
127
|
-
}
|
|
128
|
-
setClosing(false)
|
|
129
|
-
if (!isControlled) setInternalOpen(false)
|
|
130
|
-
onOpenChange?.(false)
|
|
131
|
-
}, [isControlled, onOpenChange])
|
|
132
|
-
|
|
133
|
-
// Clean up timer on unmount
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
return () => {
|
|
136
|
-
if (closingTimerRef.current) clearTimeout(closingTimerRef.current)
|
|
137
|
-
}
|
|
138
|
-
}, [])
|
|
139
|
-
|
|
140
111
|
// Handle Escape key
|
|
141
112
|
useEffect(() => {
|
|
142
|
-
if (!
|
|
113
|
+
if (!present) return
|
|
143
114
|
if (!dismissable) return
|
|
144
115
|
|
|
145
116
|
function onKeyDown(e: KeyboardEvent) {
|
|
@@ -150,23 +121,23 @@ function Root({
|
|
|
150
121
|
}
|
|
151
122
|
document.addEventListener('keydown', onKeyDown)
|
|
152
123
|
return () => document.removeEventListener('keydown', onKeyDown)
|
|
153
|
-
}, [
|
|
124
|
+
}, [present, dismissable, requestClose])
|
|
154
125
|
|
|
155
126
|
// Lock body scroll when open
|
|
156
127
|
useEffect(() => {
|
|
157
|
-
if (
|
|
128
|
+
if (present) {
|
|
158
129
|
const prev = document.body.style.overflow
|
|
159
130
|
document.body.style.overflow = 'hidden'
|
|
160
131
|
return () => {
|
|
161
132
|
document.body.style.overflow = prev
|
|
162
133
|
}
|
|
163
134
|
}
|
|
164
|
-
}, [
|
|
135
|
+
}, [present])
|
|
165
136
|
|
|
166
137
|
return (
|
|
167
138
|
<DrawerContext.Provider
|
|
168
139
|
value={{
|
|
169
|
-
open:
|
|
140
|
+
open: present,
|
|
170
141
|
closing,
|
|
171
142
|
closeDuration: closeAnimationDuration,
|
|
172
143
|
anchor,
|
|
@@ -79,6 +79,8 @@ export const styles = css.create({
|
|
|
79
79
|
// Panel sizing per anchor
|
|
80
80
|
panelHorizontal: {
|
|
81
81
|
width: '100%',
|
|
82
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
83
|
+
boxSizing: 'border-box',
|
|
82
84
|
height: '100%',
|
|
83
85
|
borderLeftWidth: borders.hairline,
|
|
84
86
|
borderLeftStyle: 'solid',
|
|
@@ -86,6 +88,8 @@ export const styles = css.create({
|
|
|
86
88
|
},
|
|
87
89
|
panelVertical: {
|
|
88
90
|
width: '100%',
|
|
91
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
92
|
+
boxSizing: 'border-box',
|
|
89
93
|
borderTopWidth: borders.hairline,
|
|
90
94
|
borderTopStyle: 'solid',
|
|
91
95
|
borderTopColor: colors.border,
|
|
@@ -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,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
|
+
}
|
|
@@ -86,6 +86,8 @@ export const styles = css.create({
|
|
|
86
86
|
// against an auto width the line breaks come out wrong and the container
|
|
87
87
|
// under-reports its height (verified on the iOS simulator).
|
|
88
88
|
width: '100%',
|
|
89
|
+
// Native has no border-box reset (the web's global-reset.css); declare it.
|
|
90
|
+
boxSizing: 'border-box',
|
|
89
91
|
},
|
|
90
92
|
// border-box is explicit: react-strict-dom mirrors the web default
|
|
91
93
|
// (content-box) on native, which adds the gap padding on top of the basis
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {createElement} from 'react'
|
|
2
|
+
import Svg, {Circle, Ellipse, Line, Path, Polygon, Polyline, Rect} from 'react-native-svg'
|
|
3
|
+
import {ICON_SIZES, type ColorToken} from '@duro-app/tokens/keys'
|
|
4
|
+
import {useResolvedColors} from '../ThemeProvider/ThemeProvider'
|
|
5
|
+
import {GLYPHS, type Paint, type Shape} from './geometry'
|
|
6
|
+
import type {IconProps} from './types'
|
|
7
|
+
|
|
8
|
+
export type {IconName, IconProps, IconSize} from './types'
|
|
9
|
+
|
|
10
|
+
const elements = {
|
|
11
|
+
path: Path,
|
|
12
|
+
circle: Circle,
|
|
13
|
+
ellipse: Ellipse,
|
|
14
|
+
line: Line,
|
|
15
|
+
rect: Rect,
|
|
16
|
+
polyline: Polyline,
|
|
17
|
+
polygon: Polygon,
|
|
18
|
+
} as const
|
|
19
|
+
|
|
20
|
+
/** react-native-svg reads `currentColor` from the Svg's `color`, which is a resolved colour here. */
|
|
21
|
+
function nativeShape(shape: Shape, key: number) {
|
|
22
|
+
const {tag, ...attrs} = shape
|
|
23
|
+
return createElement(elements[tag] as never, {key, ...attrs})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Native Icon: the same geometry as the web, drawn with react-native-svg
|
|
28
|
+
* (plan N0, Decision 9; RSD has no `svg`). It cannot inherit a text colour,
|
|
29
|
+
* so its colour is a token resolved through the nearest ThemeProvider. It is
|
|
30
|
+
* decorative: the control that hosts it carries the label.
|
|
31
|
+
*/
|
|
32
|
+
export function Icon({name, size: sizeToken = 'lg', color = 'text'}: IconProps) {
|
|
33
|
+
const size = ICON_SIZES[sizeToken]
|
|
34
|
+
const glyph = GLYPHS[name]
|
|
35
|
+
const resolved = useResolvedColors()[color as ColorToken]
|
|
36
|
+
const paint: {fill: Paint; stroke?: Paint} =
|
|
37
|
+
glyph.style === 'fill' ? {fill: 'currentColor'} : {fill: 'none', stroke: 'currentColor'}
|
|
38
|
+
return (
|
|
39
|
+
<Svg
|
|
40
|
+
viewBox="0 0 24 24"
|
|
41
|
+
width={size}
|
|
42
|
+
height={size}
|
|
43
|
+
color={resolved}
|
|
44
|
+
{...paint}
|
|
45
|
+
strokeWidth={glyph.style === 'stroke' ? 2 : undefined}
|
|
46
|
+
strokeLinecap="round"
|
|
47
|
+
strokeLinejoin="round"
|
|
48
|
+
accessibilityElementsHidden
|
|
49
|
+
importantForAccessibility="no-hide-descendants"
|
|
50
|
+
>
|
|
51
|
+
{glyph.shapes.map(nativeShape)}
|
|
52
|
+
</Svg>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -57,6 +57,7 @@ const groups: ReadonlyArray<{title: string; names: readonly IconName[]}> = [
|
|
|
57
57
|
{title: 'Input / action', names: ['search', 'mic']},
|
|
58
58
|
{title: 'Color mode', names: ['sun', 'moon', 'monitor', 'contrast']},
|
|
59
59
|
{title: 'Device status', names: ['signal', 'battery']},
|
|
60
|
+
{title: 'Media', names: ['video', 'music', 'play', 'pause']},
|
|
60
61
|
{
|
|
61
62
|
title: 'Filled variants',
|
|
62
63
|
names: [
|