@payfit/unity-illustrations 2.55.22 → 2.56.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/package.json +11 -4
- package/skills/unity-illustrations/SKILL.md +39 -0
- package/src/agent-references/illustrations-catalog.json +1996 -0
- package/src/agent-references/manifest.json +16 -0
- package/dist/esm/scripts/agent-references/illustration-reference.d.ts +0 -17
- package/src/agent-references/README.mdx +0 -17
- package/src/agent-references/illustrations.mdx +0 -74
- package/src/components/illustration/Illustration.stories.tsx +0 -362
- package/src/components/illustration/Illustration.tsx +0 -169
- package/src/components/illustration/figma/IllustrationLarge.figma.ts +0 -258
- package/src/components/illustration/figma/IllustrationMedium.figma.ts +0 -258
- package/src/components/illustration/figma/IllustrationSmall.figma.ts +0 -258
- package/src/components/lazy-illustration/LazyIllustration.stories.tsx +0 -204
- package/src/components/lazy-illustration/LazyIllustration.tsx +0 -125
- package/src/components/lazy-illustration/figma/LazyIllustrationLarge.figma.ts +0 -255
- package/src/components/lazy-illustration/figma/LazyIllustrationMedium.figma.ts +0 -255
- package/src/components/lazy-illustration/figma/LazyIllustrationSmall.figma.ts +0 -255
- package/src/docs/1-Introduction/1-Welcome.mdx +0 -111
- package/src/docs/1-Introduction/2-Getting Started.mdx +0 -25
- package/src/docs/1-Introduction/3-Usage.mdx +0 -114
- package/src/docs/1-Introduction/4-Changelog.mdx +0 -6
- package/src/docs/2-Reference/1-Find my illustration.mdx +0 -12
- package/src/docs/2-Reference/2-Find my animation.mdx +0 -14
- package/src/docs/2-Reference/3-Multi brand support.mdx +0 -146
- package/src/docs/blocks/AnimationLibrary.tsx +0 -181
- package/src/docs/blocks/BrandSupportBadge.tsx +0 -190
- package/src/docs/blocks/BrandSupportFilter.tsx +0 -72
- package/src/docs/blocks/Cards.tsx +0 -74
- package/src/docs/blocks/IllustrationLibrary.tsx +0 -179
- package/src/docs/blocks/useStorybookTheme.ts +0 -73
- package/src/index.ts +0 -4
- package/src/scripts/agent-references/illustration-reference.test.ts +0 -100
- package/src/scripts/agent-references/illustration-reference.ts +0 -140
- package/src/scripts/generate-assets.ts +0 -649
- package/src/scripts/templates/assetTemplate.ts +0 -224
- package/src/scripts/templates/baseTypes.ts +0 -45
- package/src/scripts/templates/indexTemplate.ts +0 -109
- package/src/unity.css +0 -5
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import { Canvas, Meta } from '@storybook/addon-docs/blocks'
|
|
2
|
-
import * as IllustrationStories from '../../components/illustration/Illustration.stories'
|
|
3
|
-
import * as LazyIllustrationStories from '../../components/lazy-illustration/LazyIllustration.stories'
|
|
4
|
-
|
|
5
|
-
<Meta title="Reference/Multi brand support" />
|
|
6
|
-
|
|
7
|
-
# Multi brand support
|
|
8
|
-
|
|
9
|
-
Unity illustrations can resolve different visual assets for the legacy and rebrand themes while keeping the same logical illustration name.
|
|
10
|
-
|
|
11
|
-
The theme-aware behavior is available through both rendering APIs:
|
|
12
|
-
|
|
13
|
-
- `Illustration`, when you import an asset directly from `@payfit/unity-illustrations/assets/{Name}`
|
|
14
|
-
- `LazyIllustration`, when you render an asset by its generated `name`
|
|
15
|
-
|
|
16
|
-
Both APIs ultimately render the same generated asset contract.
|
|
17
|
-
|
|
18
|
-
## Generated asset contract
|
|
19
|
-
|
|
20
|
-
Every generated asset keeps the original `url` field for backwards compatibility and can also expose a `urls` object keyed by Unity theme.
|
|
21
|
-
|
|
22
|
-
```tsx
|
|
23
|
-
const FAQ = {
|
|
24
|
-
type: 'svg',
|
|
25
|
-
url: legacySvgUrl,
|
|
26
|
-
urls: {
|
|
27
|
-
legacy: legacySvgUrl,
|
|
28
|
-
rebrand: rebrandSvgUrl,
|
|
29
|
-
},
|
|
30
|
-
animated: false,
|
|
31
|
-
name: 'FAQ',
|
|
32
|
-
}
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
`url` remains the default source. `urls.legacy` and `urls.rebrand` are used by Unity illustration components when a `UnityThemeProvider` is active.
|
|
36
|
-
|
|
37
|
-
## Runtime resolution
|
|
38
|
-
|
|
39
|
-
`Illustration` reads the active theme from `UnityThemeProvider` and resolves the rendered source as:
|
|
40
|
-
|
|
41
|
-
```tsx
|
|
42
|
-
const resolvedUrl = asset.urls?.[theme] ?? asset.url
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
`LazyIllustration` first loads the asset by name from the generated lazy map, then delegates rendering to `Illustration`. This means `LazyIllustration` has the same brand-aware behavior after the asset has loaded.
|
|
46
|
-
|
|
47
|
-
## Direct asset imports
|
|
48
|
-
|
|
49
|
-
Use direct imports when you know the asset at build time and want the import to stay explicit.
|
|
50
|
-
|
|
51
|
-
<Canvas of={IllustrationStories.ThemeVariants} meta={IllustrationStories} sourceState="shown" />
|
|
52
|
-
|
|
53
|
-
```tsx
|
|
54
|
-
import { UnityThemeProvider } from '@payfit/unity-themes'
|
|
55
|
-
import { Illustration } from '@payfit/unity-illustrations'
|
|
56
|
-
import FAQ from '@payfit/unity-illustrations/assets/FAQ'
|
|
57
|
-
|
|
58
|
-
function Example() {
|
|
59
|
-
return (
|
|
60
|
-
<UnityThemeProvider theme="rebrand">
|
|
61
|
-
<Illustration src={FAQ} variant="icon" size="lg" alt="FAQ icon" />
|
|
62
|
-
</UnityThemeProvider>
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Lazy illustrations
|
|
68
|
-
|
|
69
|
-
Use `LazyIllustration` when the illustration name is selected dynamically or when the asset should be loaded on demand.
|
|
70
|
-
|
|
71
|
-
<Canvas of={LazyIllustrationStories.ThemeVariants} meta={LazyIllustrationStories} sourceState="shown" />
|
|
72
|
-
|
|
73
|
-
```tsx
|
|
74
|
-
import { UnityThemeProvider } from '@payfit/unity-themes'
|
|
75
|
-
import { LazyIllustration } from '@payfit/unity-illustrations'
|
|
76
|
-
|
|
77
|
-
function Example() {
|
|
78
|
-
return (
|
|
79
|
-
<UnityThemeProvider theme="rebrand">
|
|
80
|
-
<LazyIllustration name="FAQ" variant="icon" size="lg" alt="FAQ icon" />
|
|
81
|
-
</UnityThemeProvider>
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## Brand support states
|
|
87
|
-
|
|
88
|
-
The Reference catalogs show a brand-support badge for each illustration. Those badges describe the visual migration state, not only whether two generated URLs exist.
|
|
89
|
-
|
|
90
|
-
### Dual brand
|
|
91
|
-
|
|
92
|
-
An illustration is considered `Dual brand` when the legacy and rebrand sources are both available and their optimized asset contents are different.
|
|
93
|
-
|
|
94
|
-
When the active theme is `legacy`, the legacy source is rendered. When the active theme is `rebrand`, the rebrand source is rendered.
|
|
95
|
-
|
|
96
|
-
### Legacy only
|
|
97
|
-
|
|
98
|
-
An illustration is considered `Legacy only` when there is no distinct rebrand visual yet.
|
|
99
|
-
|
|
100
|
-
This includes two cases:
|
|
101
|
-
|
|
102
|
-
- The generated asset only has a legacy/default source.
|
|
103
|
-
- A rebrand source file exists, but its optimized contents are identical to the legacy SVG.
|
|
104
|
-
|
|
105
|
-
In the second case, the runtime may still resolve `urls.rebrand` under the rebrand theme, but the rendered image is visually the same as the legacy asset. The catalog keeps this in `Legacy only` so it can be used as a migration checklist.
|
|
106
|
-
|
|
107
|
-
### Rebrand only
|
|
108
|
-
|
|
109
|
-
An illustration is considered `Rebrand only` when it exists only in the rebrand catalog.
|
|
110
|
-
|
|
111
|
-
For compatibility, these assets populate every URL field with the rebrand source:
|
|
112
|
-
|
|
113
|
-
```tsx
|
|
114
|
-
const RebrandOnlyAsset = {
|
|
115
|
-
url: rebrandSvgUrl,
|
|
116
|
-
urls: {
|
|
117
|
-
legacy: rebrandSvgUrl,
|
|
118
|
-
rebrand: rebrandSvgUrl,
|
|
119
|
-
},
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
This means the same rebrand image renders in both themes.
|
|
124
|
-
|
|
125
|
-
## Catalog filters
|
|
126
|
-
|
|
127
|
-
Use the brand-support filters in [Find my illustration](/?path=/docs/reference-find-my-illustration--docs) and [Find my animation](/?path=/docs/reference-find-my-animation--docs) to inspect migration progress:
|
|
128
|
-
|
|
129
|
-
- `All` shows every generated asset in that catalog.
|
|
130
|
-
- `Dual` shows assets that visibly switch between legacy and rebrand.
|
|
131
|
-
- `Legacy only` shows assets that still render legacy visuals.
|
|
132
|
-
- `Rebrand only` shows assets that only exist in the rebrand catalog.
|
|
133
|
-
|
|
134
|
-
Animated WEBP assets keep the same generated image URL unless a future asset pipeline introduces theme-specific animated sources.
|
|
135
|
-
|
|
136
|
-
## Figma automation
|
|
137
|
-
|
|
138
|
-
The Figma sync no longer distinguishes between illustrated icons and illustrated pictures. Static exports are treated as illustrations and are written to `assets/rebrand/` with the exact Figma variant filename.
|
|
139
|
-
|
|
140
|
-
The current source nodes are configured in `.figma-sync.json` under `download.illustrations.nodeIds`:
|
|
141
|
-
|
|
142
|
-
```json
|
|
143
|
-
["19134:5", "19134:8607", "19134:9880"]
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
`assets/legacy/` is the frozen fallback catalog. The sync does not create, update, or delete files there. Filename properties such as `Type`, `Name`, `With background`, and `Size` remain the source of truth for generating the public Unity illustration name.
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import React, { memo, useCallback, useMemo, useState } from 'react'
|
|
2
|
-
|
|
3
|
-
import { UnityThemeProvider } from '@payfit/unity-themes'
|
|
4
|
-
|
|
5
|
-
import { LazyIllustration } from '../../components/lazy-illustration/LazyIllustration'
|
|
6
|
-
import type { UnityIllustrationName } from '../../generated/illustrationAssets'
|
|
7
|
-
|
|
8
|
-
import { illustrationNames } from '../../generated/illustrationAssets'
|
|
9
|
-
import type { BrandSupportFilter } from './BrandSupportFilter'
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
BrandSupportBadge,
|
|
13
|
-
useBrandSupportRecords,
|
|
14
|
-
} from './BrandSupportBadge'
|
|
15
|
-
import {
|
|
16
|
-
BrandSupportFilterControl,
|
|
17
|
-
matchesBrandSupportFilter,
|
|
18
|
-
} from './BrandSupportFilter'
|
|
19
|
-
import { useStorybookTheme } from './useStorybookTheme'
|
|
20
|
-
|
|
21
|
-
const debounce = <P = unknown,>(
|
|
22
|
-
callback: (...args: P[]) => unknown,
|
|
23
|
-
wait: number,
|
|
24
|
-
) => {
|
|
25
|
-
let timeoutId: number
|
|
26
|
-
return (...args: P[]) => {
|
|
27
|
-
if (timeoutId) {
|
|
28
|
-
window.clearTimeout(timeoutId)
|
|
29
|
-
}
|
|
30
|
-
timeoutId = window.setTimeout(() => {
|
|
31
|
-
callback(...args)
|
|
32
|
-
}, wait)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const AnimationItem = memo(
|
|
37
|
-
({
|
|
38
|
-
children,
|
|
39
|
-
name,
|
|
40
|
-
copyText,
|
|
41
|
-
onClick,
|
|
42
|
-
}: React.PropsWithChildren<{
|
|
43
|
-
name: UnityIllustrationName
|
|
44
|
-
copyText: string
|
|
45
|
-
onClick: () => void
|
|
46
|
-
}>) => {
|
|
47
|
-
return (
|
|
48
|
-
<li>
|
|
49
|
-
<button
|
|
50
|
-
onClick={onClick}
|
|
51
|
-
className="uy:group uy:flex uy:h-full uy:w-full uy:flex-col uy:gap-150 uy:p-200 uy:transition-all uy:rounded-lg uy:bg-surface-neutral uy:shadow-raising uy:hover:shadow-flying uy:focus-visible:shadow-flying uy:focus-visible:outline-none uy:focus-visible:ring-2 uy:focus-visible:ring-ring uy:focus-visible:ring-offset-2 uy:focus-visible:ring-utility-focus-ring uy:focus-visible:border-border-form-hover uy:focus-visible:bg-surface-neutral-low-hover uy:border uy:border-solid uy:border-border-neutral"
|
|
52
|
-
>
|
|
53
|
-
<figure className="uy:flex uy:h-full uy:w-full uy:flex-col uy:items-center uy:gap-150">
|
|
54
|
-
<figcaption className="uy:w-full uy:text-left">
|
|
55
|
-
<span className="uy:block uy:w-full uy:truncate uy:font-mono uy:text-50 uy:leading-normal uy:text-content-neutral">
|
|
56
|
-
{name}
|
|
57
|
-
</span>
|
|
58
|
-
</figcaption>
|
|
59
|
-
<div
|
|
60
|
-
className="uy:flex uy:h-[160px] uy:w-full uy:items-center uy:justify-center uy:transition-all uy:group-hover:scale-105"
|
|
61
|
-
style={{ maxWidth: '200px' }}
|
|
62
|
-
>
|
|
63
|
-
{children}
|
|
64
|
-
</div>
|
|
65
|
-
<div className="uy:mt-auto uy:flex uy:w-full uy:items-center uy:justify-between uy:gap-100">
|
|
66
|
-
<BrandSupportBadge name={name} />
|
|
67
|
-
<span className="uy:typography-action-small uy:bg-transparent uy:theme-legacy:text-content-primary uy:theme-rebrand:text-content-neutral uy:theme-rebrand:underline uy:whitespace-nowrap">
|
|
68
|
-
{copyText}
|
|
69
|
-
</span>
|
|
70
|
-
</div>
|
|
71
|
-
</figure>
|
|
72
|
-
</button>
|
|
73
|
-
</li>
|
|
74
|
-
)
|
|
75
|
-
},
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
const AnimationLibrary = () => {
|
|
79
|
-
const theme = useStorybookTheme()
|
|
80
|
-
const [query, setQuery] = useState<string>('')
|
|
81
|
-
const [copiedAnimation, setCopiedAnimation] = useState<string | null>(null)
|
|
82
|
-
const [brandFilter, setBrandFilter] = useState<BrandSupportFilter>('all')
|
|
83
|
-
|
|
84
|
-
// Filter only animation names (those ending with 'Animation')
|
|
85
|
-
const animationList = useMemo(() => {
|
|
86
|
-
return illustrationNames.filter(name => name.endsWith('Animation'))
|
|
87
|
-
}, [])
|
|
88
|
-
const brandSupportByName = useBrandSupportRecords(animationList)
|
|
89
|
-
|
|
90
|
-
const copyToClipboard = useCallback((animationName: string) => {
|
|
91
|
-
const importStatement = `import ${animationName} from '@payfit/unity-illustrations/assets/${animationName}'`
|
|
92
|
-
|
|
93
|
-
navigator.clipboard
|
|
94
|
-
.writeText(importStatement)
|
|
95
|
-
.then(() => {
|
|
96
|
-
setCopiedAnimation(animationName)
|
|
97
|
-
setTimeout(() => {
|
|
98
|
-
setCopiedAnimation(null)
|
|
99
|
-
}, 5000)
|
|
100
|
-
})
|
|
101
|
-
.catch((error: unknown) => {
|
|
102
|
-
console.error('Failed to copy: ', error)
|
|
103
|
-
})
|
|
104
|
-
}, [])
|
|
105
|
-
|
|
106
|
-
const updateSearchQuery = debounce((animationName: string) => {
|
|
107
|
-
setQuery(animationName)
|
|
108
|
-
}, 150)
|
|
109
|
-
|
|
110
|
-
const animationListMatchingSearch = animationList.filter(animationName =>
|
|
111
|
-
animationName.toLowerCase().includes(query.toLowerCase()) &&
|
|
112
|
-
matchesBrandSupportFilter(
|
|
113
|
-
brandSupportByName.get(animationName) ?? 'loading',
|
|
114
|
-
brandFilter,
|
|
115
|
-
),
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<UnityThemeProvider key={theme} theme={theme}>
|
|
120
|
-
<section className="uy:flex uy:flex-col uy:gap-300 uy:max-w-[80vmax] uy:mx-auto">
|
|
121
|
-
<header>
|
|
122
|
-
<h3>Animation Gallery</h3>
|
|
123
|
-
<p className="uy:m-0!">
|
|
124
|
-
Click on any animation to copy its import path to the clipboard. All animations are displayed with a maximum width of 600px to ensure optimal viewing.
|
|
125
|
-
</p>
|
|
126
|
-
</header>
|
|
127
|
-
<div className="uy:flex uy:flex-col uy:gap-150">
|
|
128
|
-
<BrandSupportFilterControl
|
|
129
|
-
name="animation-brand-support-filter"
|
|
130
|
-
value={brandFilter}
|
|
131
|
-
onChange={setBrandFilter}
|
|
132
|
-
/>
|
|
133
|
-
<input
|
|
134
|
-
type="search"
|
|
135
|
-
aria-label="Search for an animation"
|
|
136
|
-
placeholder="Search for an animation..."
|
|
137
|
-
className="uy:flex uy:h-500 uy:w-full uy:text-content-form-enabled uy:rounded-100 uy:border uy:border-solid uy:border-border-form-enabled uy:bg-background uy:px-150 uy:py-100 uy:typography-body uy:placeholder:text-content-neutral-lowest uy:hover:border-border-form-hover uy:focus-visible:outline-none uy:focus-visible:ring-2 uy:focus-visible:ring-ring uy:focus-visible:ring-offset-2 uy:focus-visible:ring-utility-focus-ring"
|
|
138
|
-
onChange={e => {
|
|
139
|
-
updateSearchQuery(e.target.value)
|
|
140
|
-
}}
|
|
141
|
-
/>
|
|
142
|
-
</div>
|
|
143
|
-
|
|
144
|
-
{animationListMatchingSearch.length === 0 ? (
|
|
145
|
-
<div className="uy:text-center uy:py-800">
|
|
146
|
-
<p className="uy:typography-body uy:text-content-tertiary">
|
|
147
|
-
{query
|
|
148
|
-
? `No animations found matching "${query}"`
|
|
149
|
-
: 'No animations available'}
|
|
150
|
-
</p>
|
|
151
|
-
</div>
|
|
152
|
-
) : (
|
|
153
|
-
<ul className="sb-unstyled uy:list-none uy:grid uy:grid-cols-[repeat(auto-fill,minmax(200px,_1fr))] uy:gap-200">
|
|
154
|
-
{animationListMatchingSearch.map(name => {
|
|
155
|
-
return (
|
|
156
|
-
<AnimationItem
|
|
157
|
-
name={name}
|
|
158
|
-
copyText={
|
|
159
|
-
copiedAnimation === name ? 'Copied!' : 'Click to Copy'
|
|
160
|
-
}
|
|
161
|
-
key={name}
|
|
162
|
-
onClick={() => {
|
|
163
|
-
copyToClipboard(name)
|
|
164
|
-
}}
|
|
165
|
-
>
|
|
166
|
-
<LazyIllustration
|
|
167
|
-
variant="picture"
|
|
168
|
-
alt={`${name} animation`}
|
|
169
|
-
name={name}
|
|
170
|
-
/>
|
|
171
|
-
</AnimationItem>
|
|
172
|
-
)
|
|
173
|
-
})}
|
|
174
|
-
</ul>
|
|
175
|
-
)}
|
|
176
|
-
</section>
|
|
177
|
-
</UnityThemeProvider>
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export default AnimationLibrary
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { useSyncExternalStore } from 'react'
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
UnityIllustrationAsset,
|
|
5
|
-
UnityIllustrationName,
|
|
6
|
-
} from '../../generated/illustrationAssets'
|
|
7
|
-
|
|
8
|
-
import { lazyIllustrationsMap } from '../../generated/illustrationAssets'
|
|
9
|
-
|
|
10
|
-
export type BrandSupport = 'loading' | 'dual' | 'legacy-only' | 'rebrand-only'
|
|
11
|
-
|
|
12
|
-
const brandSupportByName = new Map<UnityIllustrationName, BrandSupport>()
|
|
13
|
-
const pendingNames = new Set<UnityIllustrationName>()
|
|
14
|
-
const listenersByName = new Map<UnityIllustrationName, Set<() => void>>()
|
|
15
|
-
const listeners = new Set<() => void>()
|
|
16
|
-
let version = 0
|
|
17
|
-
|
|
18
|
-
async function haveSameContent(firstUrl: string, secondUrl: string) {
|
|
19
|
-
if (typeof fetch === 'undefined') {
|
|
20
|
-
return false
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const [firstResponse, secondResponse] = await Promise.all([
|
|
24
|
-
fetch(firstUrl),
|
|
25
|
-
fetch(secondUrl),
|
|
26
|
-
])
|
|
27
|
-
const [firstBuffer, secondBuffer] = await Promise.all([
|
|
28
|
-
firstResponse.arrayBuffer(),
|
|
29
|
-
secondResponse.arrayBuffer(),
|
|
30
|
-
])
|
|
31
|
-
|
|
32
|
-
if (firstBuffer.byteLength !== secondBuffer.byteLength) {
|
|
33
|
-
return false
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const firstBytes = new Uint8Array(firstBuffer)
|
|
37
|
-
const secondBytes = new Uint8Array(secondBuffer)
|
|
38
|
-
|
|
39
|
-
return firstBytes.every((byte, index) => byte === secondBytes[index])
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function classifyBrandSupport(
|
|
43
|
-
asset: UnityIllustrationAsset,
|
|
44
|
-
): Promise<BrandSupport> {
|
|
45
|
-
const legacyUrl = asset.urls?.legacy ?? asset.url
|
|
46
|
-
const rebrandUrl = asset.urls?.rebrand ?? asset.url
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
legacyUrl !== rebrandUrl &&
|
|
50
|
-
!(await haveSameContent(legacyUrl, rebrandUrl))
|
|
51
|
-
) {
|
|
52
|
-
return 'dual'
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (legacyUrl.includes('.rebrand.')) {
|
|
56
|
-
return 'rebrand-only'
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return 'legacy-only'
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function notify(name: UnityIllustrationName) {
|
|
63
|
-
version += 1
|
|
64
|
-
listenersByName.get(name)?.forEach(listener => {
|
|
65
|
-
listener()
|
|
66
|
-
})
|
|
67
|
-
listeners.forEach(listener => {
|
|
68
|
-
listener()
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function loadBrandSupport(name: UnityIllustrationName) {
|
|
73
|
-
if (brandSupportByName.has(name) || pendingNames.has(name)) {
|
|
74
|
-
return
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
pendingNames.add(name)
|
|
78
|
-
|
|
79
|
-
void lazyIllustrationsMap[name]()
|
|
80
|
-
.then(async module => {
|
|
81
|
-
brandSupportByName.set(
|
|
82
|
-
name,
|
|
83
|
-
await classifyBrandSupport(module.default),
|
|
84
|
-
)
|
|
85
|
-
})
|
|
86
|
-
.catch(() => {
|
|
87
|
-
brandSupportByName.set(name, 'legacy-only')
|
|
88
|
-
})
|
|
89
|
-
.finally(() => {
|
|
90
|
-
pendingNames.delete(name)
|
|
91
|
-
notify(name)
|
|
92
|
-
})
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function subscribeToBrandSupport(
|
|
96
|
-
name: UnityIllustrationName,
|
|
97
|
-
onStoreChange: () => void,
|
|
98
|
-
) {
|
|
99
|
-
const listeners = listenersByName.get(name) ?? new Set<() => void>()
|
|
100
|
-
listeners.add(onStoreChange)
|
|
101
|
-
listenersByName.set(name, listeners)
|
|
102
|
-
|
|
103
|
-
loadBrandSupport(name)
|
|
104
|
-
|
|
105
|
-
return () => {
|
|
106
|
-
listeners.delete(onStoreChange)
|
|
107
|
-
|
|
108
|
-
if (listeners.size === 0) {
|
|
109
|
-
listenersByName.delete(name)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function getBrandSupportSnapshot(name: UnityIllustrationName): BrandSupport {
|
|
115
|
-
return brandSupportByName.get(name) ?? 'loading'
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function useBrandSupport(name: UnityIllustrationName): BrandSupport {
|
|
119
|
-
return useSyncExternalStore(
|
|
120
|
-
onStoreChange => subscribeToBrandSupport(name, onStoreChange),
|
|
121
|
-
() => getBrandSupportSnapshot(name),
|
|
122
|
-
() => 'loading',
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function subscribeToBrandSupportRecords(
|
|
127
|
-
names: readonly UnityIllustrationName[],
|
|
128
|
-
onStoreChange: () => void,
|
|
129
|
-
) {
|
|
130
|
-
listeners.add(onStoreChange)
|
|
131
|
-
names.forEach(loadBrandSupport)
|
|
132
|
-
|
|
133
|
-
return () => {
|
|
134
|
-
listeners.delete(onStoreChange)
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function useBrandSupportRecords(
|
|
139
|
-
names: readonly UnityIllustrationName[],
|
|
140
|
-
) {
|
|
141
|
-
useSyncExternalStore(
|
|
142
|
-
onStoreChange => subscribeToBrandSupportRecords(names, onStoreChange),
|
|
143
|
-
() => version,
|
|
144
|
-
() => version,
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
return new Map(
|
|
148
|
-
names.map(name => [name, getBrandSupportSnapshot(name)] as const),
|
|
149
|
-
)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const badgeContent: Record<
|
|
153
|
-
BrandSupport,
|
|
154
|
-
{ label: string; className: string; title: string }
|
|
155
|
-
> = {
|
|
156
|
-
loading: {
|
|
157
|
-
label: 'Checking brand',
|
|
158
|
-
className: 'uy:bg-surface-neutral-low uy:text-content-neutral',
|
|
159
|
-
title: 'Checking illustration brand support',
|
|
160
|
-
},
|
|
161
|
-
dual: {
|
|
162
|
-
label: 'Dual brand',
|
|
163
|
-
className: 'uy:bg-surface-success-low uy:text-content-success-high',
|
|
164
|
-
title: 'This illustration switches between legacy and rebrand assets',
|
|
165
|
-
},
|
|
166
|
-
'legacy-only': {
|
|
167
|
-
label: 'Legacy only',
|
|
168
|
-
className: 'uy:bg-surface-warning-low uy:text-content-warning-high',
|
|
169
|
-
title: 'This illustration uses the legacy asset for every theme',
|
|
170
|
-
},
|
|
171
|
-
'rebrand-only': {
|
|
172
|
-
label: 'Rebrand only',
|
|
173
|
-
className: 'uy:bg-surface-info-low uy:text-content-info-high',
|
|
174
|
-
title: 'This illustration uses the rebrand asset for every theme',
|
|
175
|
-
},
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function BrandSupportBadge({ name }: { name: UnityIllustrationName }) {
|
|
179
|
-
const brandSupport = useBrandSupport(name)
|
|
180
|
-
const { label, className, title } = badgeContent[brandSupport]
|
|
181
|
-
|
|
182
|
-
return (
|
|
183
|
-
<span
|
|
184
|
-
className={`uy:typography-action-small uy:inline-flex uy:h-300 uy:items-center uy:rounded-50 uy:px-100 ${className}`}
|
|
185
|
-
title={title}
|
|
186
|
-
>
|
|
187
|
-
{label}
|
|
188
|
-
</span>
|
|
189
|
-
)
|
|
190
|
-
}
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import type { BrandSupport } from './BrandSupportBadge'
|
|
2
|
-
|
|
3
|
-
export type BrandSupportFilter = Exclude<BrandSupport, 'loading'> | 'all'
|
|
4
|
-
|
|
5
|
-
const options: ReadonlyArray<{
|
|
6
|
-
label: string
|
|
7
|
-
value: BrandSupportFilter
|
|
8
|
-
}> = [
|
|
9
|
-
{ label: 'All', value: 'all' },
|
|
10
|
-
{ label: 'Dual', value: 'dual' },
|
|
11
|
-
{ label: 'Legacy only', value: 'legacy-only' },
|
|
12
|
-
{ label: 'Rebrand only', value: 'rebrand-only' },
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
export function matchesBrandSupportFilter(
|
|
16
|
-
brandSupport: BrandSupport,
|
|
17
|
-
filter: BrandSupportFilter,
|
|
18
|
-
) {
|
|
19
|
-
return filter === 'all' || brandSupport === filter
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function BrandSupportFilterControl({
|
|
23
|
-
name,
|
|
24
|
-
value,
|
|
25
|
-
onChange,
|
|
26
|
-
}: {
|
|
27
|
-
name: string
|
|
28
|
-
value: BrandSupportFilter
|
|
29
|
-
onChange: (value: BrandSupportFilter) => void
|
|
30
|
-
}) {
|
|
31
|
-
return (
|
|
32
|
-
<fieldset className="uy:m-0 uy:border-0 uy:p-0">
|
|
33
|
-
<legend className="uy:sr-only">Filter illustrations by brand support</legend>
|
|
34
|
-
<div className="uy:flex uy:w-fit uy:gap-50 uy:rounded-75 uy:bg-surface-neutral-low uy:px-50 uy:py-50">
|
|
35
|
-
{options.map(option => {
|
|
36
|
-
const isSelected = option.value === value
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<label
|
|
40
|
-
key={option.value}
|
|
41
|
-
className={`uy:group uy:flex uy:h-400 uy:cursor-pointer uy:items-center uy:gap-25 uy:rounded-50 uy:px-150 uy:py-50 ${
|
|
42
|
-
isSelected
|
|
43
|
-
? 'uy:bg-surface-neutral-enabled uy:shadow-raising'
|
|
44
|
-
: 'uy:hover:bg-surface-neutral-hover'
|
|
45
|
-
}`}
|
|
46
|
-
>
|
|
47
|
-
<input
|
|
48
|
-
type="radio"
|
|
49
|
-
name={name}
|
|
50
|
-
value={option.value}
|
|
51
|
-
checked={isSelected}
|
|
52
|
-
onChange={() => {
|
|
53
|
-
onChange(option.value)
|
|
54
|
-
}}
|
|
55
|
-
className="uy:sr-only"
|
|
56
|
-
/>
|
|
57
|
-
<span
|
|
58
|
-
className={`uy:flex uy:items-center uy:gap-50 uy:typography-action ${
|
|
59
|
-
isSelected
|
|
60
|
-
? 'uy:text-content-neutral'
|
|
61
|
-
: 'uy:text-content-neutral-enabled uy:group-hover:text-content-neutral-hover'
|
|
62
|
-
}`}
|
|
63
|
-
>
|
|
64
|
-
{option.label}
|
|
65
|
-
</span>
|
|
66
|
-
</label>
|
|
67
|
-
)
|
|
68
|
-
})}
|
|
69
|
-
</div>
|
|
70
|
-
</fieldset>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
|
|
3
|
-
type CardProps = React.PropsWithChildren
|
|
4
|
-
|
|
5
|
-
export const Card: React.FC<CardProps> = ({ children }) => {
|
|
6
|
-
return (
|
|
7
|
-
<article className="sb-unstyled uy:bg-surface-neutral uy:border uy:border-solid uy:border-border-neutral uy:shadow-raising uy:rounded-200 uy:p-300 uy:flex uy:flex-col uy:gap-150">
|
|
8
|
-
{children}
|
|
9
|
-
</article>
|
|
10
|
-
)
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const CardWithCover: React.FC<
|
|
14
|
-
CardProps & { coverSrc: string; coverAlt: string }
|
|
15
|
-
> = ({ children, coverSrc, coverAlt }) => {
|
|
16
|
-
return (
|
|
17
|
-
<article className="sb-unstyled uy:bg-surface-neutral uy:border uy:border-solid uy:border-border-neutral uy:shadow-raising uy:rounded-200 uy:flex uy:flex-col uy:overflow-hidden uy:sm:flex-row">
|
|
18
|
-
<img
|
|
19
|
-
src={coverSrc}
|
|
20
|
-
alt={coverAlt}
|
|
21
|
-
className="sb-unstyled uy:w-full uy:h-[180px] uy:object-contain uy:bg-blue-l3 uy:sm:object-fill uy:sm:h-full uy:sm:max-w-[200px]"
|
|
22
|
-
/>
|
|
23
|
-
<section className="uy:p-300 uy:space-y-100">{children}</section>
|
|
24
|
-
</article>
|
|
25
|
-
)
|
|
26
|
-
}
|
|
27
|
-
export const CardHeader: React.FC<React.PropsWithChildren> = ({ children }) => {
|
|
28
|
-
return <header>{children}</header>
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const CardTitle: React.FC<React.PropsWithChildren> = ({ children }) => {
|
|
32
|
-
return <h3 className="sb-unstyled uy:typography-h3 uy:m-0">{children}</h3>
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const CardContent: React.FC<React.PropsWithChildren> = ({
|
|
36
|
-
children,
|
|
37
|
-
}) => {
|
|
38
|
-
return <section className="uy:typography-body">{children}</section>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const CardActions: React.FC<React.PropsWithChildren> = ({
|
|
42
|
-
children,
|
|
43
|
-
}) => {
|
|
44
|
-
return <footer className="uy:flex uy:flex-col uy:gap-100">{children}</footer>
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const Link: React.FC<React.ComponentProps<'a'>> = ({
|
|
48
|
-
children,
|
|
49
|
-
...rest
|
|
50
|
-
}) => {
|
|
51
|
-
return (
|
|
52
|
-
<a
|
|
53
|
-
className="uy:typography-action uy:block uy:text-content-primary uy:underline"
|
|
54
|
-
{...rest}
|
|
55
|
-
>
|
|
56
|
-
{children}
|
|
57
|
-
</a>
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
type CardGridProps = {
|
|
62
|
-
children: React.ReactNode
|
|
63
|
-
cols?: number
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const CardGrid: React.FC<CardGridProps> = ({ children, cols = 2 }) => {
|
|
67
|
-
return (
|
|
68
|
-
<div
|
|
69
|
-
className={`uy:grid uy:grid-cols-1 uy:gap-150 uy:md:grid-cols-${cols}`}
|
|
70
|
-
>
|
|
71
|
-
{children}
|
|
72
|
-
</div>
|
|
73
|
-
)
|
|
74
|
-
}
|