@duro-app/ui 5.33.0 → 5.34.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.
Files changed (59) hide show
  1. package/dist/{Table-ypIvytAS.js → Table-IuD0tooi.js} +56 -49
  2. package/dist/Table-IuD0tooi.js.map +1 -0
  3. package/dist/{compiler-runtime-BuugS4D0.js → compiler-runtime-CV6vsf8h.js} +80 -73
  4. package/dist/{compiler-runtime-BuugS4D0.js.map → compiler-runtime-CV6vsf8h.js.map} +1 -1
  5. package/dist/components/Avatar/Avatar.d.ts +8 -0
  6. package/dist/components/Avatar/Avatar.d.ts.map +1 -0
  7. package/dist/components/Avatar/Avatar.meta.d.ts +3 -0
  8. package/dist/components/Avatar/Avatar.meta.d.ts.map +1 -0
  9. package/dist/components/Avatar/Avatar.stories.d.ts +15 -0
  10. package/dist/components/Avatar/Avatar.stories.d.ts.map +1 -0
  11. package/dist/components/Avatar/styles.css.d.ts +41 -0
  12. package/dist/components/Avatar/styles.css.d.ts.map +1 -0
  13. package/dist/components/Avatar/types.d.ts +21 -0
  14. package/dist/components/Avatar/types.d.ts.map +1 -0
  15. package/dist/components/Icon/Icon.d.ts.map +1 -1
  16. package/dist/components/Icon/geometry.d.ts +4 -0
  17. package/dist/components/Icon/geometry.d.ts.map +1 -1
  18. package/dist/components/Icon/phosphor-glyphs.d.ts +1028 -0
  19. package/dist/components/Icon/phosphor-glyphs.d.ts.map +1 -0
  20. package/dist/components/Icon/types.d.ts +3 -1
  21. package/dist/components/Icon/types.d.ts.map +1 -1
  22. package/dist/components/List/ItemLink.d.ts +32 -0
  23. package/dist/components/List/ItemLink.d.ts.map +1 -0
  24. package/dist/components/List/List.d.ts +36 -4
  25. package/dist/components/List/List.d.ts.map +1 -1
  26. package/dist/components/List/List.stories.d.ts +9 -0
  27. package/dist/components/List/List.stories.d.ts.map +1 -1
  28. package/dist/components/List/styles.css.d.ts +39 -0
  29. package/dist/components/List/styles.css.d.ts.map +1 -1
  30. package/dist/form.js +1 -1
  31. package/dist/index.css +1 -1
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +5921 -4688
  35. package/dist/index.js.map +1 -1
  36. package/dist/table.js +2 -2
  37. package/package.json +3 -2
  38. package/src/components/Avatar/Avatar.meta.ts +32 -0
  39. package/src/components/Avatar/Avatar.native.tsx +37 -0
  40. package/src/components/Avatar/Avatar.stories.tsx +92 -0
  41. package/src/components/Avatar/Avatar.tsx +40 -0
  42. package/src/components/Avatar/styles.css.ts +32 -0
  43. package/src/components/Avatar/types.ts +21 -0
  44. package/src/components/Icon/Icon.meta.ts +1 -1
  45. package/src/components/Icon/Icon.native.tsx +2 -2
  46. package/src/components/Icon/Icon.tsx +4 -3
  47. package/src/components/Icon/geometry.ts +7 -0
  48. package/src/components/Icon/phosphor-glyphs.ts +1285 -0
  49. package/src/components/Icon/phosphor-names.json +130 -0
  50. package/src/components/Icon/types.ts +4 -0
  51. package/src/components/List/ItemLink.native.tsx +60 -0
  52. package/src/components/List/ItemLink.tsx +80 -0
  53. package/src/components/List/List.meta.ts +3 -3
  54. package/src/components/List/List.stories.tsx +91 -1
  55. package/src/components/List/List.tsx +130 -22
  56. package/src/components/List/styles.css.ts +45 -0
  57. package/src/index.native.ts +6 -0
  58. package/src/index.ts +6 -0
  59. package/dist/Table-ypIvytAS.js.map +0 -1
package/dist/table.js CHANGED
@@ -1,6 +1,6 @@
1
- import { M as F, q as gt, R as it, H as kt, t as pt, v as bt, w as ft, x as mt, b as ht, g as yt, r as St, y as Tt } from "./Table-ypIvytAS.js";
1
+ import { M as F, q as gt, R as it, H as kt, t as pt, v as bt, w as ft, x as mt, b as ht, g as yt, r as St, y as Tt } from "./Table-IuD0tooi.js";
2
2
  import { jsx as u, jsxs as q } from "react/jsx-runtime";
3
- import { c as H, m as p, d as b } from "./compiler-runtime-BuugS4D0.js";
3
+ import { c as H, m as p, d as b } from "./compiler-runtime-CV6vsf8h.js";
4
4
  import { flexRender as E, useReactTable as rt, getPaginationRowModel as Ct, getFilteredRowModel as vt, getSortedRowModel as at, getCoreRowModel as st } from "@tanstack/react-table";
5
5
  import { useState as G, useCallback as wt, useRef as K, useEffect as Z, useLayoutEffect as zt } from "react";
6
6
  import { useVirtualizer as Vt } from "@tanstack/react-virtual";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "5.33.0",
3
+ "version": "5.34.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -92,11 +92,12 @@
92
92
  "dependencies": {
93
93
  "@tanstack/react-virtual": "^3.14.13",
94
94
  "qrcode": "^1.5.4",
95
- "@duro-app/tokens": "^5.33.0"
95
+ "@duro-app/tokens": "^5.34.1"
96
96
  },
97
97
  "devDependencies": {
98
98
  "@babel/preset-typescript": "^7.28.0",
99
99
  "@hookform/resolvers": "^5.0.0",
100
+ "@phosphor-icons/core": "2.1.1",
100
101
  "@types/qrcode": "^1.5.6",
101
102
  "@types/react": "^19.0.0",
102
103
  "@types/react-dom": "^19.0.0",
@@ -0,0 +1,32 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'A person or entity as a picture, else their initials, else a neutral placeholder. alt is required and is the accessible name in all three states. sm, md and lg map to the controlSm, thumbSm and iconXxl size tokens; shape is circle or square. svg takes a complete SVG document string generated by the app (never user-supplied markup) and draws it filling the frame. Precedence: src (until it fails to load) > svg > initials > blank.',
6
+ whenToUse: [
7
+ 'The owner, author or assignee of a record, beside their name',
8
+ 'A List.Leading slot for people or accounts',
9
+ ],
10
+ whenNotToUse: [
11
+ 'A glyph for a thing or a status — use Icon or StatusIcon',
12
+ 'A photo that is the content — use an image element inside Card',
13
+ ],
14
+ relatedTo: [
15
+ {
16
+ component: 'List',
17
+ kind: 'composition',
18
+ relationship: 'Avatar fills List.Leading for people and accounts',
19
+ },
20
+ {
21
+ component: 'Icon',
22
+ kind: 'contrast',
23
+ relationship: 'Icon draws a glyph; Avatar identifies someone',
24
+ },
25
+ ],
26
+ example: `<Inline gap="sm" align="center">
27
+ <Avatar src="/people/ada.png" alt="Ada Lovelace" />
28
+ <Avatar alt="Generated" svg={generatedArt} />
29
+ <Avatar alt="Grace Hopper" initials="GH" size="lg" />
30
+ <Avatar alt="Unassigned" shape="square" />
31
+ </Inline>`,
32
+ }
@@ -0,0 +1,37 @@
1
+ import {useState} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {SvgXml} from 'react-native-svg'
4
+ import {styles} from './styles.css'
5
+ import type {AvatarProps} from './types'
6
+
7
+ export type {AvatarProps, AvatarShape, AvatarSize} from './types'
8
+
9
+ /**
10
+ * Native Avatar: the web's precedence (src until it fails, svg, initials, blank). The svg document
11
+ * is drawn with react-native-svg's SvgXml, since RSD has no `svg`.
12
+ */
13
+ export function Avatar({src, svg, alt, initials, size = 'md', shape = 'circle'}: AvatarProps) {
14
+ const [failedSrc, setFailedSrc] = useState<string | null>(null)
15
+ const showImage = src !== undefined && src !== failedSrc
16
+ const frame = [styles.base, styles[size], styles[shape]]
17
+
18
+ if (showImage) {
19
+ return (
20
+ <html.div style={frame}>
21
+ <html.img src={src} alt={alt} onError={() => setFailedSrc(src)} style={styles.image} />
22
+ </html.div>
23
+ )
24
+ }
25
+ if (svg !== undefined) {
26
+ return (
27
+ <html.div role="img" aria-label={alt} style={frame}>
28
+ <SvgXml xml={svg} width="100%" height="100%" />
29
+ </html.div>
30
+ )
31
+ }
32
+ return (
33
+ <html.div role="img" aria-label={alt} style={frame}>
34
+ {initials ? <html.span aria-hidden={true}>{initials}</html.span> : null}
35
+ </html.div>
36
+ )
37
+ }
@@ -0,0 +1,92 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect, waitFor} from 'storybook/test'
3
+ import {Avatar} from './Avatar'
4
+ import {Inline} from '../Inline/Inline'
5
+ import {onThemeSurface} from '../../docs/themedSurface'
6
+
7
+ const meta: Meta<typeof Avatar> = {
8
+ title: 'Components/Avatar',
9
+ component: Avatar,
10
+ decorators: [onThemeSurface],
11
+ argTypes: {
12
+ size: {control: 'select', options: ['sm', 'md', 'lg']},
13
+ shape: {control: 'select', options: ['circle', 'square']},
14
+ },
15
+ }
16
+
17
+ export default meta
18
+ type Story = StoryObj<typeof Avatar>
19
+
20
+ // A 1x1 transparent GIF: a real image that needs no network.
21
+ const PICTURE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
22
+
23
+ export const Default: Story = {
24
+ args: {alt: 'Grace Hopper', initials: 'GH'},
25
+ }
26
+
27
+ /** The image when there is a src; else the initials; else a neutral placeholder. */
28
+ export const ImageInitialsPlaceholder: Story = {
29
+ render: () => (
30
+ <Inline gap="md" align="center">
31
+ <Avatar src={PICTURE} alt="Ada Lovelace" />
32
+ <Avatar alt="Grace Hopper" initials="GH" />
33
+ <Avatar alt="Unassigned" />
34
+ </Inline>
35
+ ),
36
+ play: async ({canvas}) => {
37
+ const image = canvas.getByRole('img', {name: 'Ada Lovelace'})
38
+ await expect(image.tagName).toBe('IMG')
39
+ await expect(image).toHaveAttribute('src', PICTURE)
40
+
41
+ const initials = canvas.getByRole('img', {name: 'Grace Hopper'})
42
+ await expect(initials.tagName).not.toBe('IMG')
43
+ await expect(initials).toHaveTextContent('GH')
44
+
45
+ const placeholder = canvas.getByRole('img', {name: 'Unassigned'})
46
+ await expect(placeholder).toBeEmptyDOMElement()
47
+ },
48
+ }
49
+
50
+ export const Sizes: Story = {
51
+ render: () => (
52
+ <Inline gap="md" align="center">
53
+ <Avatar alt="Small" initials="S" size="sm" />
54
+ <Avatar alt="Medium" initials="M" size="md" />
55
+ <Avatar alt="Large" initials="L" size="lg" />
56
+ </Inline>
57
+ ),
58
+ }
59
+
60
+ export const Shapes: Story = {
61
+ render: () => (
62
+ <Inline gap="md" align="center">
63
+ <Avatar alt="Circle" initials="C" shape="circle" />
64
+ <Avatar alt="Square" initials="S" shape="square" />
65
+ </Inline>
66
+ ),
67
+ }
68
+
69
+ /** An image that cannot load falls back to the initials. */
70
+ export const BrokenImage: Story = {
71
+ args: {src: 'data:image/png;base64,not-an-image', alt: 'Alan Turing', initials: 'AT'},
72
+ play: async ({canvas}) => {
73
+ await waitFor(async () => {
74
+ const avatar = canvas.getByRole('img', {name: 'Alan Turing'})
75
+ await expect(avatar.tagName).not.toBe('IMG')
76
+ await expect(avatar).toHaveTextContent('AT')
77
+ })
78
+ },
79
+ }
80
+
81
+ const ART =
82
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 1"><rect width="1" height="1" fill="#c33"/><rect x="1" width="1" height="1" fill="#36c"/></svg>'
83
+
84
+ /** An app-generated SVG document fills the frame when there is no loadable src. */
85
+ export const GeneratedSvg: Story = {
86
+ args: {svg: ART, alt: 'Generated art'},
87
+ play: async ({canvas}) => {
88
+ const image = canvas.getByRole('img', {name: 'Generated art'})
89
+ await expect(image.tagName).toBe('IMG')
90
+ await expect(image.getAttribute('src')).toMatch(/^data:image\/svg\+xml/)
91
+ },
92
+ }
@@ -0,0 +1,40 @@
1
+ import {useState} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {styles} from './styles.css'
4
+ import type {AvatarProps} from './types'
5
+
6
+ export type {AvatarProps, AvatarShape, AvatarSize} from './types'
7
+
8
+ /**
9
+ * A person or entity as a picture, else an app-generated SVG, else their initials, else a neutral
10
+ * blank. Each carries `alt` as the accessible name.
11
+ */
12
+ export function Avatar({src, svg, alt, initials, size = 'md', shape = 'circle'}: AvatarProps) {
13
+ const [failedSrc, setFailedSrc] = useState<string | null>(null)
14
+ const showImage = src !== undefined && src !== failedSrc
15
+ const frame = [styles.base, styles[size], styles[shape]]
16
+
17
+ if (showImage) {
18
+ return (
19
+ <html.div style={frame}>
20
+ <html.img src={src} alt={alt} onError={() => setFailedSrc(src)} style={styles.image} />
21
+ </html.div>
22
+ )
23
+ }
24
+ if (svg !== undefined) {
25
+ return (
26
+ <html.div style={frame}>
27
+ <html.img
28
+ src={`data:image/svg+xml,${encodeURIComponent(svg)}`}
29
+ alt={alt}
30
+ style={styles.image}
31
+ />
32
+ </html.div>
33
+ )
34
+ }
35
+ return (
36
+ <html.div role="img" aria-label={alt} style={frame}>
37
+ {initials ? <html.span aria-hidden={true}>{initials}</html.span> : null}
38
+ </html.div>
39
+ )
40
+ }
@@ -0,0 +1,32 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
5
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
6
+
7
+ export const styles = css.create({
8
+ base: {
9
+ display: 'flex',
10
+ alignItems: 'center',
11
+ justifyContent: 'center',
12
+ flexShrink: 0,
13
+ overflow: 'hidden',
14
+ backgroundColor: colors.neutralBg,
15
+ color: colors.neutralText,
16
+ fontWeight: typography.fontWeightMedium,
17
+ lineHeight: 1,
18
+ },
19
+
20
+ sm: {width: sizes.controlSm, height: sizes.controlSm, fontSize: typography.fontSizeXs},
21
+ md: {width: sizes.thumbSm, height: sizes.thumbSm, fontSize: typography.fontSizeSm},
22
+ lg: {width: sizes.iconXxl, height: sizes.iconXxl, fontSize: typography.fontSizeMd},
23
+
24
+ circle: {borderRadius: radii.full},
25
+ square: {borderRadius: radii.sm},
26
+
27
+ image: {
28
+ width: '100%',
29
+ height: '100%',
30
+ objectFit: 'cover',
31
+ },
32
+ })
@@ -0,0 +1,21 @@
1
+ export type AvatarSize = 'sm' | 'md' | 'lg'
2
+ export type AvatarShape = 'circle' | 'square'
3
+
4
+ export interface AvatarProps {
5
+ /** Image URL. Without it, or when it fails to load, the svg, else the initials, show. */
6
+ src?: string
7
+ /** The accessible name: who or what this avatar stands for. Never empty. */
8
+ alt: string
9
+ /**
10
+ * A complete SVG document string generated by the app (for example id-seeded art), drawn as the
11
+ * picture, filling the frame. Never user-supplied markup. Used when there is no `src`, or `src`
12
+ * failed to load; it wins over the initials.
13
+ */
14
+ svg?: string
15
+ /** Shown when there is no image or svg; one or two characters. */
16
+ initials?: string
17
+ /** sm 28, md 40, lg 48 (the controlSm, thumbSm and iconXxl size tokens). Default md. */
18
+ size?: AvatarSize
19
+ /** Default circle. */
20
+ shape?: AvatarShape
21
+ }
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'SVG icon component. Renders stroke or filled icons from the design system icon set. Size scales with the size prop. Use inside buttons, badges, or standalone.',
5
+ 'SVG icon component. Renders stroke or filled icons from the design system icon set, plus Phosphor glyphs (regular weight, fill style, generated by scripts/generate-phosphor-icons.mjs from phosphor-names.json) under their Phosphor names. Size scales with the size prop. Use inside buttons, badges, or standalone.',
6
6
  whenToUse: [
7
7
  'Button icons (inside Button children)',
8
8
  'Status indicators alongside text',
@@ -2,7 +2,7 @@ import {createElement} from 'react'
2
2
  import Svg, {Circle, Ellipse, Line, Path, Polygon, Polyline, Rect} from 'react-native-svg'
3
3
  import {ICON_SIZES, type ColorToken} from '@duro-app/tokens/keys'
4
4
  import {useResolvedColors} from '../ThemeProvider/ThemeProvider'
5
- import {GLYPHS, type Paint, type Shape} from './geometry'
5
+ import {DEFAULT_VIEW_BOX, GLYPHS, type Paint, type Shape} from './geometry'
6
6
  import type {IconProps} from './types'
7
7
 
8
8
  export type {IconName, IconProps, IconSize} from './types'
@@ -37,7 +37,7 @@ export function Icon({name, size: sizeToken = 'lg', color = 'text'}: IconProps)
37
37
  glyph.style === 'fill' ? {fill: 'currentColor'} : {fill: 'none', stroke: 'currentColor'}
38
38
  return (
39
39
  <Svg
40
- viewBox="0 0 24 24"
40
+ viewBox={glyph.viewBox ?? DEFAULT_VIEW_BOX}
41
41
  width={size}
42
42
  height={size}
43
43
  color={resolved}
@@ -2,7 +2,7 @@ import {createElement} from 'react'
2
2
  import {ICON_SIZES} from '@duro-app/tokens/keys'
3
3
  import {colors} from '@duro-app/tokens/tokens/colors.css'
4
4
 
5
- import {GLYPHS, type Shape} from './geometry'
5
+ import {DEFAULT_VIEW_BOX, GLYPHS, type Shape} from './geometry'
6
6
  import type {IconProps} from './types'
7
7
 
8
8
  export type {IconName, IconProps, IconSize} from './types'
@@ -16,11 +16,12 @@ function svgShape(shape: Shape, key: number) {
16
16
  export function Icon({name, size: sizeToken = 'lg', color}: IconProps) {
17
17
  const size = ICON_SIZES[sizeToken]
18
18
  const glyph = GLYPHS[name]
19
+ const viewBox = glyph.viewBox ?? DEFAULT_VIEW_BOX
19
20
  const shapes = glyph.shapes.map(svgShape)
20
21
  const style = color ? {color: colors[color]} : undefined
21
22
  if (glyph.style === 'fill') {
22
23
  return (
23
- <svg viewBox="0 0 24 24" fill="currentColor" width={size} height={size} style={style}>
24
+ <svg viewBox={viewBox} fill="currentColor" width={size} height={size} style={style}>
24
25
  {shapes}
25
26
  </svg>
26
27
  )
@@ -28,7 +29,7 @@ export function Icon({name, size: sizeToken = 'lg', color}: IconProps) {
28
29
 
29
30
  return (
30
31
  <svg
31
- viewBox="0 0 24 24"
32
+ viewBox={viewBox}
32
33
  fill="none"
33
34
  stroke="currentColor"
34
35
  strokeWidth={2}
@@ -1,3 +1,4 @@
1
+ import {PHOSPHOR_GLYPHS} from './phosphor-glyphs'
1
2
  import type {IconName} from './types'
2
3
 
3
4
  /**
@@ -33,10 +34,16 @@ export type Shape =
33
34
 
34
35
  export interface Glyph {
35
36
  readonly style: 'stroke' | 'fill'
37
+ /** The glyph's own coordinate space; the 24-unit grid when absent. */
38
+ readonly viewBox?: string
36
39
  readonly shapes: readonly Shape[]
37
40
  }
38
41
 
42
+ /** The grid Duro's own glyphs are drawn on. */
43
+ export const DEFAULT_VIEW_BOX = '0 0 24 24'
44
+
39
45
  export const GLYPHS: Record<IconName, Glyph> = {
46
+ ...PHOSPHOR_GLYPHS,
40
47
  'x-circle': {
41
48
  style: 'stroke',
42
49
  shapes: [