Skip to content

Commit 69e837f

Browse files
authored
feat(book): add injection and popups categories to the component book (#12445)
1 parent 65d5d27 commit 69e837f

34 files changed

Lines changed: 944 additions & 347 deletions

apps/book/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,27 @@ export default function Demo() {
3636
`import.meta.glob` in `src/registry.ts` picks it up automatically. The first path segment
3737
under `demos/` is the sidebar section (`foundation`, `components`, `icons`, `shared`, …).
3838

39+
A section can have sub-categories one level deep: `demos/<section>/<group>/<name>.demo.tsx`
40+
(e.g. `demos/injection/twitter/Banner.demo.tsx`) renders under a "Twitter" sub-header inside the
41+
"Injection" section. `injection` is the only section that currently uses this.
42+
43+
### Injection and Popups
44+
45+
- `injection` demos the components each site adaptor (`packages/mask/content-script/site-adaptors/*`)
46+
injects into a social platform's own page, one sub-category per platform. Most of the real
47+
injection code is tightly coupled to the extension runtime (background RPC via the `#services`
48+
subpath import, the live `activatedSiteAdaptorUI` global, DOM watchers) and can't run standalone
49+
here. Where a component's pure-UI part could be cleanly separated from that coupling, it was
50+
moved into `packages/injected-ui` — a small package with **one export per component, no barrel
51+
`index.ts`** — and both the real injection code and this book import the same file from there
52+
(see e.g. `packages/mask/content-script/components/GuideStep/index.tsx`, which now just computes
53+
props from the real guide-progress state and renders `@masknet/injected-ui/GuideStep`). Only a
54+
handful of components have been split out so far; the rest are left for follow-up.
55+
- `popups` demos a few self-contained presentational components from
56+
`packages/mask/popups/components/*` (imported directly by relative path, since that package isn't
57+
set up to be depended on by name). Anything that needs live wallet/RPC state, or that pulls in a
58+
barrel file with unrelated modules, was left out — see the note in the PR that added this section.
59+
3960
## Deploy (Vercel)
4061

4162
- Root Directory: `apps/book`

apps/book/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
},
1111
"dependencies": {
1212
"@masknet/icons": "workspace:*",
13+
"@masknet/injected-ui": "workspace:*",
1314
"@masknet/shared-base": "workspace:*",
1415
"@masknet/theme": "workspace:*",
1516
"@mui/icons-material": "9.2.0",

apps/book/src/components/Nav.tsx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { sections } from '../registry.ts'
1+
import { Fragment } from 'react'
2+
import { groupTitle, sections } from '../registry.ts'
23
import { useMode } from '../providers.tsx'
34

45
interface Props {
@@ -17,21 +18,33 @@ export function Nav({ current, onNavigate }: Props) {
1718
</button>
1819
</div>
1920

20-
{sections.map((section) => (
21-
<div key={section.id}>
22-
<div className="book-section-title">{section.title}</div>
23-
{section.entries.map((entry) => (
24-
<button
25-
type="button"
26-
key={entry.id}
27-
className="book-nav-link"
28-
data-active={entry.id === current}
29-
onClick={() => onNavigate(entry.id)}>
30-
{entry.name}
31-
</button>
32-
))}
33-
</div>
34-
))}
21+
{sections.map((section) => {
22+
let lastGroup: string | undefined
23+
return (
24+
<div key={section.id}>
25+
<div className="book-section-title">{section.title}</div>
26+
{section.entries.map((entry) => {
27+
const showGroupHeader = entry.group !== lastGroup
28+
lastGroup = entry.group
29+
return (
30+
<Fragment key={entry.id}>
31+
{showGroupHeader && entry.group ?
32+
<div className="book-group-title">{groupTitle(entry.group)}</div>
33+
: null}
34+
<button
35+
type="button"
36+
className="book-nav-link"
37+
data-active={entry.id === current}
38+
data-grouped={!!entry.group}
39+
onClick={() => onNavigate(entry.id)}>
40+
{entry.name}
41+
</button>
42+
</Fragment>
43+
)
44+
})}
45+
</div>
46+
)
47+
})}
3548
</nav>
3649
)
3750
}

apps/book/src/demo.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ export interface DemoModule {
1515
}
1616

1717
export interface DemoEntry {
18-
/** e.g. "components/ActionButton" */
18+
/** e.g. "injection/twitter/Banner" */
1919
id: string
20-
/** e.g. "components" */
20+
/** e.g. "injection" */
2121
section: string
22-
/** e.g. "ActionButton" */
22+
/** e.g. "twitter", for demos nested one level under a section */
23+
group?: string
24+
/** e.g. "Banner" */
2325
name: string
2426
title: string
2527
description?: string
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useState } from 'react'
2+
import { FormControlLabel, Stack, Switch } from '@mui/material'
3+
import { Banner } from '@masknet/injected-ui/Banner'
4+
5+
export const meta = {
6+
title: 'Banner',
7+
description:
8+
'"Sign in with Mask" entry point injected into the compose box on Facebook (packages/injected-ui/src/Banner.tsx).',
9+
}
10+
11+
export default function BannerFacebookDemo() {
12+
const [connected, setConnected] = useState(false)
13+
14+
return (
15+
<Stack spacing={2} sx={{ alignItems: 'flex-start' }}>
16+
<FormControlLabel
17+
control={<Switch checked={connected} onChange={(_, checked) => setConnected(checked)} />}
18+
label="Persona already connected (hides the banner)"
19+
/>
20+
<span
21+
style={{
22+
display: 'block',
23+
padding: '0 16px',
24+
marginTop: 0,
25+
}}>
26+
<Banner nextStep={connected ? 'hidden' : { onClick: () => alert('open dashboard') }} />
27+
</span>
28+
</Stack>
29+
)
30+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { useState } from 'react'
2+
import { Stack, Typography } from '@mui/material'
3+
import { PostDialogHint } from '@masknet/injected-ui/PostDialogHint'
4+
5+
export const meta = {
6+
title: 'PostDialogHint',
7+
description:
8+
'Mask badge injected next to the compose button on Facebook (packages/injected-ui/src/PostDialogHint.tsx), with the onboarding guide tip enabled.',
9+
}
10+
11+
export default function PostDialogHintFacebookDemo() {
12+
const [clicked, setClicked] = useState(0)
13+
const [guideDismissed, setGuideDismissed] = useState(false)
14+
15+
return (
16+
<Stack spacing={2} sx={{ alignItems: 'flex-start', paddingTop: '48px', paddingLeft: '48px' }}>
17+
<Typography variant="body2" color="text.secondary">
18+
Clicked {clicked} time(s){guideDismissed ? ' — guide dismissed' : ''}
19+
</Typography>
20+
<PostDialogHint
21+
size={24}
22+
tooltip={{ disabled: false, placement: 'top' }}
23+
onHintButtonClicked={() => setClicked((c) => c + 1)}
24+
guide={
25+
guideDismissed ? undefined : (
26+
{
27+
step: 1,
28+
total: 1,
29+
tip: 'Click here to have a quick start.',
30+
visible: true,
31+
onSkip: () => setGuideDismissed(true),
32+
onNext: () => setGuideDismissed(true),
33+
onTry: () => {
34+
setGuideDismissed(true)
35+
setClicked((c) => c + 1)
36+
},
37+
}
38+
)
39+
}
40+
/>
41+
</Stack>
42+
)
43+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useState } from 'react'
2+
import { FormControlLabel, Stack, Switch } from '@mui/material'
3+
import { Banner } from '@masknet/injected-ui/Banner'
4+
5+
export const meta = {
6+
title: 'Banner',
7+
description:
8+
'"Sign in with Mask" entry point injected into the compose box on Minds (packages/injected-ui/src/Banner.tsx). Minds uses the Minds-branded icon via `iconType="minds"`.',
9+
}
10+
11+
export default function BannerMindsDemo() {
12+
const [connected, setConnected] = useState(false)
13+
14+
return (
15+
<Stack spacing={2} sx={{ alignItems: 'flex-start' }}>
16+
<FormControlLabel
17+
control={<Switch checked={connected} onChange={(_, checked) => setConnected(checked)} />}
18+
label="Persona already connected (hides the banner)"
19+
/>
20+
<Banner iconType="minds" nextStep={connected ? 'hidden' : { onClick: () => alert('open dashboard') }} />
21+
</Stack>
22+
)
23+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useState } from 'react'
2+
import { Stack, Typography } from '@mui/material'
3+
import { PostDialogHint } from '@masknet/injected-ui/PostDialogHint'
4+
5+
export const meta = {
6+
title: 'PostDialogHint',
7+
description:
8+
'Mask badge injected next to the compose button on Minds (packages/injected-ui/src/PostDialogHint.tsx). Minds disables the onboarding guide tip for this entry point.',
9+
}
10+
11+
export default function PostDialogHintMindsDemo() {
12+
const [clicked, setClicked] = useState(0)
13+
14+
return (
15+
<Stack spacing={2} sx={{ alignItems: 'flex-start' }}>
16+
<Typography variant="body2" color="text.secondary">
17+
Clicked {clicked} time(s)
18+
</Typography>
19+
<PostDialogHint
20+
size={17}
21+
iconType="minds"
22+
tooltip={{ disabled: true }}
23+
onHintButtonClicked={() => setClicked((c) => c + 1)}
24+
/>
25+
</Stack>
26+
)
27+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { useState } from 'react'
2+
import { Button, Stack, Typography } from '@mui/material'
3+
import { GuideStep } from '@masknet/injected-ui/GuideStep'
4+
5+
export const meta = {
6+
title: 'GuideStep',
7+
description:
8+
'Onboarding tooltip overlay injected next to a highlighted element on every site adaptor (packages/injected-ui/src/GuideStep.tsx). Used cross-platform: the wallet/app toolbox hints, PostDialogHint, and the setup wizard all wrap their target in this.',
9+
}
10+
11+
const TIPS = [
12+
'Explore multi-chain dApps.',
13+
'Connect and switch between your wallets.',
14+
'Click here to have a quick start.',
15+
]
16+
const TOTAL = TIPS.length
17+
18+
export default function GuideStepDemo() {
19+
const [step, setStep] = useState(1)
20+
const [finished, setFinished] = useState(false)
21+
22+
return (
23+
<Stack spacing={2} sx={{ alignItems: 'flex-start', paddingTop: '48px', paddingLeft: '48px' }}>
24+
<Typography variant="body2" color="text.secondary">
25+
{finished ? 'Tour dismissed.' : `Step ${step} of ${TOTAL}`}
26+
</Typography>
27+
<Button
28+
variant="outlined"
29+
onClick={() => {
30+
setStep(1)
31+
setFinished(false)
32+
}}>
33+
Restart tour
34+
</Button>
35+
<GuideStep
36+
total={TOTAL}
37+
step={step}
38+
tip={TIPS[step - 1]}
39+
visible={!finished}
40+
onSkip={() => setFinished(true)}
41+
onNext={() => setStep((s) => Math.min(s + 1, TOTAL))}
42+
onTry={() => setFinished(true)}>
43+
<Button variant="contained" sx={{ pointerEvents: 'none' }}>
44+
Highlighted element
45+
</Button>
46+
</GuideStep>
47+
</Stack>
48+
)
49+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useState } from 'react'
2+
import { FormControlLabel, Stack, Switch } from '@mui/material'
3+
import { Banner } from '@masknet/injected-ui/Banner'
4+
5+
export const meta = {
6+
title: 'Banner',
7+
description:
8+
'"Sign in with Mask" entry point injected into the compose box on X/Twitter (packages/injected-ui/src/Banner.tsx). Hidden once a persona is already linked to the account.',
9+
}
10+
11+
export default function BannerTwitterDemo() {
12+
const [connected, setConnected] = useState(false)
13+
14+
return (
15+
<Stack spacing={2} sx={{ alignItems: 'flex-start' }}>
16+
<FormControlLabel
17+
control={<Switch checked={connected} onChange={(_, checked) => setConnected(checked)} />}
18+
label="Persona already connected (hides the banner)"
19+
/>
20+
<Banner nextStep={connected ? 'hidden' : { onClick: () => alert('open dashboard') }} />
21+
</Stack>
22+
)
23+
}

0 commit comments

Comments
 (0)