@duro-app/ui 5.34.0 → 5.34.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "5.34.0",
3
+ "version": "5.34.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -92,7 +92,7 @@
92
92
  "dependencies": {
93
93
  "@tanstack/react-virtual": "^3.14.13",
94
94
  "qrcode": "^1.5.4",
95
- "@duro-app/tokens": "^5.34.0"
95
+ "@duro-app/tokens": "^5.34.2"
96
96
  },
97
97
  "devDependencies": {
98
98
  "@babel/preset-typescript": "^7.28.0",
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
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. An image that fails to load falls back to the initials.',
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
6
  whenToUse: [
7
7
  'The owner, author or assignee of a record, beside their name',
8
8
  'A List.Leading slot for people or accounts',
@@ -25,6 +25,7 @@ export const meta: ComponentMeta = {
25
25
  ],
26
26
  example: `<Inline gap="sm" align="center">
27
27
  <Avatar src="/people/ada.png" alt="Ada Lovelace" />
28
+ <Avatar alt="Generated" svg={generatedArt} />
28
29
  <Avatar alt="Grace Hopper" initials="GH" size="lg" />
29
30
  <Avatar alt="Unassigned" shape="square" />
30
31
  </Inline>`,
@@ -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
+ }
@@ -77,3 +77,16 @@ export const BrokenImage: Story = {
77
77
  })
78
78
  },
79
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
+ }
@@ -1,28 +1,15 @@
1
1
  import {useState} from 'react'
2
2
  import {html} from 'react-strict-dom'
3
3
  import {styles} from './styles.css'
4
+ import type {AvatarProps} from './types'
4
5
 
5
- export type AvatarSize = 'sm' | 'md' | 'lg'
6
- export type AvatarShape = 'circle' | 'square'
7
-
8
- export interface AvatarProps {
9
- /** Image URL. Without it, or when it fails to load, the initials show. */
10
- src?: string
11
- /** The accessible name: who or what this avatar stands for. Never empty. */
12
- alt: string
13
- /** Shown when there is no image; one or two characters. */
14
- initials?: string
15
- /** sm 28, md 40, lg 48 (the controlSm, thumbSm and iconXxl size tokens). Default md. */
16
- size?: AvatarSize
17
- /** Default circle. */
18
- shape?: AvatarShape
19
- }
6
+ export type {AvatarProps, AvatarShape, AvatarSize} from './types'
20
7
 
21
8
  /**
22
- * A person or entity as a picture, else their initials, else a neutral blank. The image, the
23
- * initials and the blank all carry `alt` as the accessible name.
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.
24
11
  */
25
- export function Avatar({src, alt, initials, size = 'md', shape = 'circle'}: AvatarProps) {
12
+ export function Avatar({src, svg, alt, initials, size = 'md', shape = 'circle'}: AvatarProps) {
26
13
  const [failedSrc, setFailedSrc] = useState<string | null>(null)
27
14
  const showImage = src !== undefined && src !== failedSrc
28
15
  const frame = [styles.base, styles[size], styles[shape]]
@@ -34,6 +21,17 @@ export function Avatar({src, alt, initials, size = 'md', shape = 'circle'}: Avat
34
21
  </html.div>
35
22
  )
36
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
+ }
37
35
  return (
38
36
  <html.div role="img" aria-label={alt} style={frame}>
39
37
  {initials ? <html.span aria-hidden={true}>{initials}</html.span> : null}
@@ -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,6 +2,7 @@ import type {Meta, StoryObj} from '@storybook/react'
2
2
  import {css, html} from 'react-strict-dom'
3
3
  import {Icon} from './Icon'
4
4
  import type {IconName} from './Icon'
5
+ import {PHOSPHOR_GLYPHS} from './phosphor-glyphs'
5
6
  import {spacing} from '@duro-app/tokens/tokens/spacing.css'
6
7
  import {colors} from '@duro-app/tokens/tokens/colors.css'
7
8
  import {typography} from '@duro-app/tokens/tokens/typography.css'
@@ -69,6 +70,11 @@ const groups: ReadonlyArray<{title: string; names: readonly IconName[]}> = [
69
70
  'lock-filled',
70
71
  ],
71
72
  },
73
+ // Generated from @phosphor-icons/core (scripts/generate-phosphor-icons.mjs), alphabetical.
74
+ {
75
+ title: 'Phosphor',
76
+ names: (Object.keys(PHOSPHOR_GLYPHS) as IconName[]).sort(),
77
+ },
72
78
  ]
73
79
 
74
80
  const styles = css.create({