@dfosco/storyboard-react 4.0.0-beta.46 → 4.0.0-beta.48
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 +3 -3
- package/src/AuthModal/AuthModal.jsx +72 -66
- package/src/AuthModal/AuthModal.module.css +58 -37
- package/src/BranchBar/BranchBar.jsx +7 -1
- package/src/CommandPalette/CommandPalette.jsx +16 -3
- package/src/CommandPalette/command-palette.css +57 -12
- package/src/canvas/CanvasPage.bridge.test.jsx +47 -9
- package/src/canvas/CanvasPage.jsx +1 -1
- package/src/canvas/canvasTheme.js +96 -52
- package/src/canvas/widgets/embedTheme.js +98 -55
- package/src/hooks/useThemeState.js +61 -0
- package/src/hooks/useThemeState.test.js +66 -0
- package/src/index.js +1 -0
- package/src/vite/data-plugin.js +6 -0
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.48",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@base-ui/react": "^1.4.0",
|
|
7
|
-
"@dfosco/storyboard-core": "4.0.0-beta.
|
|
8
|
-
"@dfosco/tiny-canvas": "4.0.0-beta.
|
|
7
|
+
"@dfosco/storyboard-core": "4.0.0-beta.48",
|
|
8
|
+
"@dfosco/tiny-canvas": "4.0.0-beta.48",
|
|
9
9
|
"@neodrag/react": "^2.3.1",
|
|
10
10
|
"glob": "^11.0.0",
|
|
11
11
|
"jsonc-parser": "^3.3.1",
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* - ViewfinderNew sidebar login button via same event
|
|
6
6
|
*/
|
|
7
7
|
import { useState, useEffect, useCallback } from 'react'
|
|
8
|
+
import { Dialog } from '@base-ui/react/dialog'
|
|
9
|
+
import { Button } from '@base-ui/react/button'
|
|
8
10
|
import css from './AuthModal.module.css'
|
|
9
11
|
|
|
10
12
|
const COMMENTS_TOKEN_KEY = 'sb-comments-token'
|
|
@@ -54,75 +56,79 @@ export default function AuthModal() {
|
|
|
54
56
|
if (e.key === 'Enter') handleSignIn()
|
|
55
57
|
}, [handleSignIn])
|
|
56
58
|
|
|
57
|
-
if (!open) return null
|
|
58
|
-
|
|
59
59
|
const repo = getRepoInfo()
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
|
-
<
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
62
|
+
<Dialog.Root open={open} onOpenChange={setOpen}>
|
|
63
|
+
<Dialog.Portal>
|
|
64
|
+
<Dialog.Backdrop className={css.backdrop} />
|
|
65
|
+
<div className={css.popupWrap}>
|
|
66
|
+
<Dialog.Popup className={css.popup}>
|
|
67
|
+
<Dialog.Title className={css.title}>Sign in for comments</Dialog.Title>
|
|
68
|
+
<Dialog.Description className={css.desc}>
|
|
69
|
+
Leave comments for other users to see and respond, and react to! Storyboard
|
|
70
|
+
comments use Discussions as a back-end and require a GitHub PAT to be enabled.
|
|
71
|
+
</Dialog.Description>
|
|
72
|
+
<Dialog.Close className={css.closeBtn} aria-label="Close">×</Dialog.Close>
|
|
73
|
+
|
|
74
|
+
<hr className={css.separator} />
|
|
75
|
+
|
|
76
|
+
<div className={css.tokenCard}>
|
|
77
|
+
<p className={css.tokenCardTitle}>Fine-grained Personal Access Token</p>
|
|
78
|
+
<div className={css.tokenCardRow}>
|
|
79
|
+
<span className={css.tokenCardLabel}>Owner:</span>
|
|
80
|
+
<code className={css.tokenCardCode}>{repo.owner}</code>
|
|
81
|
+
</div>
|
|
82
|
+
<div className={css.tokenCardRow}>
|
|
83
|
+
<span className={css.tokenCardLabel}>Expiration:</span>
|
|
84
|
+
<code className={css.tokenCardCode}>366 days</code>
|
|
85
|
+
<span className={css.tokenCardHint}>(recommended)</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div className={css.tokenCardRow}>
|
|
88
|
+
<span className={css.tokenCardLabel}>Repository access:</span>
|
|
89
|
+
<code className={css.tokenCardCode}>Only select repositories > {repo.owner}/{repo.name}</code>
|
|
90
|
+
</div>
|
|
91
|
+
<div className={css.tokenCardRow}>
|
|
92
|
+
<span className={css.tokenCardLabel}>Permissions:</span>
|
|
93
|
+
<code className={css.tokenCardCode}>Repositories > Discussions > Access: Read and Write</code>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<a
|
|
98
|
+
className={css.tokenLink}
|
|
99
|
+
href="https://github.com/settings/personal-access-tokens/new"
|
|
100
|
+
target="_blank"
|
|
101
|
+
rel="noopener noreferrer"
|
|
102
|
+
>
|
|
103
|
+
Create a GitHub Fine-Grained Personal Access Token ↗
|
|
104
|
+
</a>
|
|
105
|
+
|
|
106
|
+
<hr className={css.separator} />
|
|
107
|
+
|
|
108
|
+
<label className={css.label} htmlFor="auth-modal-token">Personal Access Token</label>
|
|
109
|
+
<input
|
|
110
|
+
id="auth-modal-token"
|
|
111
|
+
className={css.input}
|
|
112
|
+
placeholder="github_pat_… or ghp_…"
|
|
113
|
+
type="password"
|
|
114
|
+
autoFocus
|
|
115
|
+
value={tokenValue}
|
|
116
|
+
onChange={e => setTokenValue(e.target.value)}
|
|
117
|
+
onKeyDown={handleKeyDown}
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
<div className={css.warning}>
|
|
121
|
+
<span className={css.warningIcon}>⚠️</span>
|
|
122
|
+
<span>Comments are an experimental feature and may be unstable.</span>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div className={css.actions}>
|
|
126
|
+
<Dialog.Close className={css.btnSecondary}>Cancel</Dialog.Close>
|
|
127
|
+
<button className={css.btnPrimary} onClick={handleSignIn}>Sign in</button>
|
|
128
|
+
</div>
|
|
129
|
+
</Dialog.Popup>
|
|
124
130
|
</div>
|
|
125
|
-
</
|
|
126
|
-
</
|
|
131
|
+
</Dialog.Portal>
|
|
132
|
+
</Dialog.Root>
|
|
127
133
|
)
|
|
128
134
|
}
|
|
@@ -1,22 +1,35 @@
|
|
|
1
|
-
/* AuthModal — PAT entry dialog */
|
|
1
|
+
/* AuthModal — PAT entry dialog (BaseUI Dialog) */
|
|
2
2
|
|
|
3
|
-
.
|
|
3
|
+
.backdrop {
|
|
4
4
|
position: fixed;
|
|
5
5
|
inset: 0;
|
|
6
|
-
background: rgba(0, 0, 0, 0.4);
|
|
6
|
+
background: var(--overlay-backdrop-bgColor, rgba(0, 0, 0, 0.4));
|
|
7
|
+
z-index: 10000;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.popupWrap {
|
|
11
|
+
position: fixed;
|
|
12
|
+
inset: 0;
|
|
13
|
+
z-index: 10001;
|
|
7
14
|
display: flex;
|
|
8
15
|
align-items: center;
|
|
9
16
|
justify-content: center;
|
|
10
|
-
|
|
17
|
+
pointer-events: none;
|
|
11
18
|
}
|
|
12
19
|
|
|
13
|
-
.
|
|
14
|
-
|
|
20
|
+
.popupWrap > * {
|
|
21
|
+
pointer-events: auto;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.popup {
|
|
25
|
+
background: var(--bgColor-default);
|
|
15
26
|
border-radius: 12px;
|
|
16
|
-
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.12);
|
|
27
|
+
box-shadow: var(--shadow-overlay, 0 16px 48px rgba(0, 0, 0, 0.12));
|
|
17
28
|
padding: 28px;
|
|
18
|
-
width:
|
|
19
|
-
max-
|
|
29
|
+
max-width: 620px;
|
|
30
|
+
max-height: 90vh;
|
|
31
|
+
overflow-y: auto;
|
|
32
|
+
color: var(--fgColor-default);
|
|
20
33
|
position: relative;
|
|
21
34
|
}
|
|
22
35
|
|
|
@@ -28,43 +41,43 @@
|
|
|
28
41
|
border: none;
|
|
29
42
|
font-size: 24px;
|
|
30
43
|
line-height: 1;
|
|
31
|
-
color:
|
|
44
|
+
color: var(--fgColor-muted);
|
|
32
45
|
cursor: pointer;
|
|
33
46
|
padding: 4px 8px;
|
|
34
47
|
border-radius: 4px;
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
.closeBtn:hover {
|
|
38
|
-
background:
|
|
39
|
-
color:
|
|
51
|
+
background: var(--bgColor-neutral-muted);
|
|
52
|
+
color: var(--fgColor-default);
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
.title {
|
|
43
56
|
font-size: 18px;
|
|
44
57
|
font-weight: 600;
|
|
45
58
|
margin-bottom: 8px;
|
|
46
|
-
color:
|
|
59
|
+
color: var(--fgColor-default);
|
|
47
60
|
padding-right: 32px;
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
.desc {
|
|
51
64
|
font-size: 15px;
|
|
52
|
-
color:
|
|
65
|
+
color: var(--fgColor-muted);
|
|
53
66
|
margin-bottom: 16px;
|
|
54
67
|
line-height: 1.5;
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
.separator {
|
|
58
71
|
border: none;
|
|
59
|
-
border-top: 1px solid
|
|
72
|
+
border-top: 1px solid var(--borderColor-default);
|
|
60
73
|
margin: 16px 0;
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
/* Token config card — matches GitHub PAT settings screenshot */
|
|
64
77
|
|
|
65
78
|
.tokenCard {
|
|
66
|
-
background:
|
|
67
|
-
border: 1px solid
|
|
79
|
+
background: var(--bgColor-muted);
|
|
80
|
+
border: 1px solid var(--borderColor-default);
|
|
68
81
|
border-radius: 8px;
|
|
69
82
|
padding: 14px 16px;
|
|
70
83
|
margin-bottom: 12px;
|
|
@@ -73,7 +86,7 @@
|
|
|
73
86
|
.tokenCardTitle {
|
|
74
87
|
font-size: 15px;
|
|
75
88
|
font-weight: 600;
|
|
76
|
-
color:
|
|
89
|
+
color: var(--fgColor-default);
|
|
77
90
|
margin-bottom: 8px;
|
|
78
91
|
}
|
|
79
92
|
|
|
@@ -83,7 +96,7 @@
|
|
|
83
96
|
gap: 6px;
|
|
84
97
|
font-size: 14px;
|
|
85
98
|
line-height: 1.8;
|
|
86
|
-
color:
|
|
99
|
+
color: var(--fgColor-muted);
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
.tokenCardLabel {
|
|
@@ -93,20 +106,20 @@
|
|
|
93
106
|
.tokenCardCode {
|
|
94
107
|
font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
|
|
95
108
|
font-size: 13px;
|
|
96
|
-
color:
|
|
97
|
-
background:
|
|
109
|
+
color: var(--fgColor-default);
|
|
110
|
+
background: var(--bgColor-default);
|
|
98
111
|
padding: 1px 6px;
|
|
99
112
|
border-radius: 4px;
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
.tokenCardHint {
|
|
103
|
-
color:
|
|
116
|
+
color: var(--fgColor-muted);
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
.tokenLink {
|
|
107
120
|
display: inline-block;
|
|
108
121
|
font-size: 15px;
|
|
109
|
-
color:
|
|
122
|
+
color: var(--fgColor-accent);
|
|
110
123
|
text-decoration: none;
|
|
111
124
|
margin-bottom: 4px;
|
|
112
125
|
}
|
|
@@ -121,24 +134,31 @@
|
|
|
121
134
|
display: block;
|
|
122
135
|
font-size: 15px;
|
|
123
136
|
font-weight: 600;
|
|
124
|
-
color:
|
|
137
|
+
color: var(--fgColor-default);
|
|
125
138
|
margin-bottom: 6px;
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
.input {
|
|
129
142
|
width: 100%;
|
|
130
143
|
padding: 10px 12px;
|
|
131
|
-
border: 1px solid
|
|
144
|
+
border: 1px solid var(--borderColor-default);
|
|
132
145
|
border-radius: 6px;
|
|
133
146
|
font-size: 15px;
|
|
134
147
|
font-family: 'SF Mono', SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
|
|
135
148
|
margin-bottom: 16px;
|
|
136
149
|
box-sizing: border-box;
|
|
150
|
+
background: var(--bgColor-default);
|
|
151
|
+
color: var(--fgColor-default);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.input::placeholder {
|
|
155
|
+
color: var(--fgColor-muted);
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
.input:focus {
|
|
140
159
|
outline: none;
|
|
141
|
-
border-color:
|
|
160
|
+
border-color: var(--borderColor-accent-emphasis);
|
|
161
|
+
box-shadow: 0 0 0 3px var(--borderColor-accent-muted);
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
/* Warning banner */
|
|
@@ -148,11 +168,11 @@
|
|
|
148
168
|
align-items: center;
|
|
149
169
|
gap: 8px;
|
|
150
170
|
padding: 10px 12px;
|
|
151
|
-
background:
|
|
152
|
-
border: 1px solid
|
|
171
|
+
background: var(--bgColor-attention-muted);
|
|
172
|
+
border: 1px solid var(--borderColor-attention-muted);
|
|
153
173
|
border-radius: 6px;
|
|
154
174
|
font-size: 14px;
|
|
155
|
-
color:
|
|
175
|
+
color: var(--fgColor-attention);
|
|
156
176
|
margin-bottom: 8px;
|
|
157
177
|
}
|
|
158
178
|
|
|
@@ -172,29 +192,30 @@
|
|
|
172
192
|
|
|
173
193
|
.btnSecondary {
|
|
174
194
|
padding: 8px 16px;
|
|
175
|
-
background:
|
|
176
|
-
border:
|
|
195
|
+
background: transparent;
|
|
196
|
+
border: none;
|
|
177
197
|
border-radius: 6px;
|
|
178
198
|
font-size: 15px;
|
|
179
|
-
color:
|
|
199
|
+
color: var(--fgColor-muted);
|
|
180
200
|
cursor: pointer;
|
|
181
201
|
}
|
|
182
202
|
|
|
183
203
|
.btnSecondary:hover {
|
|
184
|
-
|
|
204
|
+
color: var(--fgColor-default);
|
|
205
|
+
background: var(--bgColor-neutral-muted);
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
.btnPrimary {
|
|
188
|
-
padding: 8px
|
|
189
|
-
background:
|
|
209
|
+
padding: 8px 20px;
|
|
210
|
+
background: var(--bgColor-accent-emphasis);
|
|
190
211
|
border: none;
|
|
191
212
|
border-radius: 6px;
|
|
192
213
|
font-size: 15px;
|
|
193
214
|
font-weight: 600;
|
|
194
|
-
color:
|
|
215
|
+
color: var(--fgColor-onEmphasis);
|
|
195
216
|
cursor: pointer;
|
|
196
217
|
}
|
|
197
218
|
|
|
198
219
|
.btnPrimary:hover {
|
|
199
|
-
background:
|
|
220
|
+
background: var(--bgColor-accent-emphasis-hover, var(--bgColor-accent-emphasis));
|
|
200
221
|
}
|
|
@@ -11,6 +11,12 @@ import css from './BranchBar.module.css'
|
|
|
11
11
|
export default function BranchBar({ basePath }) {
|
|
12
12
|
const [hidden, setHidden] = useState(false)
|
|
13
13
|
|
|
14
|
+
const isHiddenByParam = useMemo(() => {
|
|
15
|
+
if (typeof window === 'undefined') return false
|
|
16
|
+
const params = new URLSearchParams(window.location.search)
|
|
17
|
+
return params.has('_sb_hide_branch_bar') || params.has('_sb_embed')
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
14
20
|
const currentBranch = useMemo(() => {
|
|
15
21
|
const m = (basePath || '').match(/\/branch--([^/]+)\/?$/)
|
|
16
22
|
return m ? m[1] : 'main'
|
|
@@ -26,7 +32,7 @@ export default function BranchBar({ basePath }) {
|
|
|
26
32
|
return () => observer.disconnect()
|
|
27
33
|
}, [])
|
|
28
34
|
|
|
29
|
-
if (!isOnBranch || hidden) return null
|
|
35
|
+
if (!isOnBranch || hidden || isHiddenByParam) return null
|
|
30
36
|
|
|
31
37
|
function hideChrome() {
|
|
32
38
|
window.dispatchEvent(new KeyboardEvent('keydown', {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
getCommandPaletteConfig,
|
|
18
18
|
getToolbarConfig,
|
|
19
19
|
setTheme,
|
|
20
|
+
getTheme,
|
|
20
21
|
isExcludedByRoute,
|
|
21
22
|
} from '@dfosco/storyboard-core'
|
|
22
23
|
import CreateDialog from './CreateDialog.jsx'
|
|
@@ -675,6 +676,14 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
675
676
|
const [authorIndex, setAuthorIndex] = useState(new Map())
|
|
676
677
|
const [activePage, setActivePage] = useState('root')
|
|
677
678
|
const [createType, setCreateType] = useState(null)
|
|
679
|
+
const [currentTheme, setCurrentTheme] = useState(() => getTheme())
|
|
680
|
+
|
|
681
|
+
// Keep currentTheme in sync when theme changes
|
|
682
|
+
useEffect(() => {
|
|
683
|
+
const handler = (e) => setCurrentTheme(e.detail.theme)
|
|
684
|
+
document.addEventListener('storyboard:theme:changed', handler)
|
|
685
|
+
return () => document.removeEventListener('storyboard:theme:changed', handler)
|
|
686
|
+
}, [])
|
|
678
687
|
|
|
679
688
|
function handleCreateAction(type) {
|
|
680
689
|
setOpen(false)
|
|
@@ -773,7 +782,9 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
773
782
|
id: `subpage:${menu.id}`,
|
|
774
783
|
items: (menu.options || []).map((opt, i) => ({
|
|
775
784
|
id: `subpage:${menu.id}:${i}`,
|
|
776
|
-
children: opt.
|
|
785
|
+
children: opt.toolHandler === 'core:theme' && opt.value === currentTheme
|
|
786
|
+
? <span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}><span>{opt.label}</span><span>✓</span></span>
|
|
787
|
+
: opt.label,
|
|
777
788
|
keywords: [opt.label, menu.label || menu.id],
|
|
778
789
|
showType: false,
|
|
779
790
|
onClick: () => {
|
|
@@ -789,7 +800,7 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
789
800
|
},
|
|
790
801
|
})),
|
|
791
802
|
})).filter(g => g.items.length > 0)
|
|
792
|
-
}, [toolMenus])
|
|
803
|
+
}, [toolMenus, currentTheme])
|
|
793
804
|
|
|
794
805
|
const filteredItems = useMemo(() => {
|
|
795
806
|
const base = filterItems(items, search)
|
|
@@ -903,7 +914,9 @@ export default function StoryboardCommandPalette({ basePath }) {
|
|
|
903
914
|
setActivePage('root')
|
|
904
915
|
}}
|
|
905
916
|
>
|
|
906
|
-
{opt.
|
|
917
|
+
{opt.toolHandler === 'core:theme' && opt.value === currentTheme
|
|
918
|
+
? <span style={{ display: 'flex', width: '100%', justifyContent: 'space-between', alignItems: 'center' }}><span>{opt.label}</span><span>✓</span></span>
|
|
919
|
+
: opt.label}
|
|
907
920
|
</CommandPalette.ListItem>
|
|
908
921
|
))}
|
|
909
922
|
</CommandPalette.List>
|
|
@@ -7,56 +7,101 @@
|
|
|
7
7
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/*
|
|
11
|
+
* Light mode overrides for react-cmdk.
|
|
12
|
+
* react-cmdk uses @media (prefers-color-scheme: dark) with Tailwind dark:
|
|
13
|
+
* utilities. When the OS is dark but the app theme is light, those media
|
|
14
|
+
* queries fire and the palette renders dark. We override every dark: utility
|
|
15
|
+
* back to light values when data-color-mode="light".
|
|
16
|
+
*/
|
|
17
|
+
html[data-color-mode="light"] .command-palette .dark\:bg-gray-900,
|
|
18
|
+
html[data-color-mode="light"] .command-palette .dark\:bg-gray-800 {
|
|
19
|
+
background-color: rgb(255 255 255) !important;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
html[data-color-mode="light"] .command-palette .dark\:text-white {
|
|
23
|
+
color: inherit !important;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
html[data-color-mode="light"] .command-palette .dark\:text-gray-600 {
|
|
27
|
+
color: rgb(107 114 128) !important;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
html[data-color-mode="light"] .command-palette .dark\:hover\:bg-gray-800:hover {
|
|
31
|
+
background-color: rgb(243 244 246) !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
html[data-color-mode="light"] .command-palette .dark\:hover\:text-gray-300:hover {
|
|
35
|
+
color: rgb(107 114 128) !important;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
html[data-color-mode="light"] .command-palette .dark\:divide-gray-800 > :not([hidden]) ~ :not([hidden]) {
|
|
39
|
+
border-color: rgb(229 231 235) !important;
|
|
40
|
+
}
|
|
41
|
+
|
|
10
42
|
/*
|
|
11
43
|
* Dark mode overrides for react-cmdk.
|
|
12
44
|
* react-cmdk uses @media (prefers-color-scheme: dark) internally,
|
|
13
|
-
* but we drive dark mode via data-color-mode on
|
|
14
|
-
*
|
|
45
|
+
* but we drive dark mode via data-color-mode on <html> (set by themeStore).
|
|
46
|
+
* Primer may also mirror it to <body>, so we target both.
|
|
15
47
|
*/
|
|
16
|
-
body[data-color-mode="dark"] .command-palette
|
|
48
|
+
body[data-color-mode="dark"] .command-palette,
|
|
49
|
+
html[data-color-mode="dark"] .command-palette {
|
|
17
50
|
color-scheme: dark;
|
|
18
51
|
}
|
|
19
52
|
|
|
20
|
-
body[data-color-mode="dark"] .command-palette .command-palette-content
|
|
53
|
+
body[data-color-mode="dark"] .command-palette .command-palette-content,
|
|
54
|
+
html[data-color-mode="dark"] .command-palette .command-palette-content {
|
|
21
55
|
color: #e6edf3 !important;
|
|
22
56
|
}
|
|
23
57
|
|
|
24
58
|
/* Panel background */
|
|
25
|
-
body[data-color-mode="dark"] .command-palette .bg-white
|
|
59
|
+
body[data-color-mode="dark"] .command-palette .bg-white,
|
|
60
|
+
html[data-color-mode="dark"] .command-palette .bg-white {
|
|
26
61
|
background-color: #2d333b !important;
|
|
27
62
|
}
|
|
28
63
|
|
|
29
64
|
/* Hover highlight */
|
|
30
|
-
body[data-color-mode="dark"] .command-palette .hover\:bg-gray-100:hover
|
|
65
|
+
body[data-color-mode="dark"] .command-palette .hover\:bg-gray-100:hover,
|
|
66
|
+
html[data-color-mode="dark"] .command-palette .hover\:bg-gray-100:hover {
|
|
31
67
|
background-color: #373e47 !important;
|
|
32
68
|
}
|
|
33
69
|
|
|
34
70
|
/* Selected/active item highlight */
|
|
35
|
-
body[data-color-mode="dark"] .command-palette .bg-gray-200\/50
|
|
71
|
+
body[data-color-mode="dark"] .command-palette .bg-gray-200\/50,
|
|
72
|
+
html[data-color-mode="dark"] .command-palette .bg-gray-200\/50 {
|
|
36
73
|
background-color: rgba(99, 110, 123, 0.4) !important;
|
|
37
74
|
}
|
|
38
75
|
|
|
39
76
|
/* Muted text (headings, badges, "Action" labels) */
|
|
40
77
|
body[data-color-mode="dark"] .command-palette .text-gray-400,
|
|
41
|
-
body[data-color-mode="dark"] .command-palette .text-gray-500
|
|
78
|
+
body[data-color-mode="dark"] .command-palette .text-gray-500,
|
|
79
|
+
html[data-color-mode="dark"] .command-palette .text-gray-400,
|
|
80
|
+
html[data-color-mode="dark"] .command-palette .text-gray-500 {
|
|
42
81
|
color: #768390 !important;
|
|
43
82
|
}
|
|
44
83
|
|
|
45
84
|
/* Input text */
|
|
46
|
-
body[data-color-mode="dark"] .command-palette input
|
|
85
|
+
body[data-color-mode="dark"] .command-palette input,
|
|
86
|
+
html[data-color-mode="dark"] .command-palette input {
|
|
47
87
|
color: #e6edf3 !important;
|
|
48
88
|
}
|
|
49
|
-
body[data-color-mode="dark"] .command-palette .placeholder-gray-500::placeholder
|
|
89
|
+
body[data-color-mode="dark"] .command-palette .placeholder-gray-500::placeholder,
|
|
90
|
+
html[data-color-mode="dark"] .command-palette .placeholder-gray-500::placeholder {
|
|
50
91
|
color: #636e7b !important;
|
|
51
92
|
}
|
|
52
|
-
body[data-color-mode="dark"] .command-palette .placeholder-gray-500::-moz-placeholder
|
|
93
|
+
body[data-color-mode="dark"] .command-palette .placeholder-gray-500::-moz-placeholder,
|
|
94
|
+
html[data-color-mode="dark"] .command-palette .placeholder-gray-500::-moz-placeholder {
|
|
53
95
|
color: #636e7b !important;
|
|
54
96
|
}
|
|
55
97
|
|
|
56
98
|
/* Borders / dividers */
|
|
57
99
|
body[data-color-mode="dark"] .command-palette .divide-y > :not([hidden]) ~ :not([hidden]),
|
|
58
100
|
body[data-color-mode="dark"] .command-palette .border-t,
|
|
59
|
-
body[data-color-mode="dark"] .command-palette .border-b
|
|
101
|
+
body[data-color-mode="dark"] .command-palette .border-b,
|
|
102
|
+
html[data-color-mode="dark"] .command-palette .divide-y > :not([hidden]) ~ :not([hidden]),
|
|
103
|
+
html[data-color-mode="dark"] .command-palette .border-t,
|
|
104
|
+
html[data-color-mode="dark"] .command-palette .border-b {
|
|
60
105
|
border-color: #444c56 !important;
|
|
61
106
|
}
|
|
62
107
|
|
|
@@ -283,17 +283,40 @@ describe('CanvasPage canvas bridge', () => {
|
|
|
283
283
|
})
|
|
284
284
|
|
|
285
285
|
describe('getCanvasThemeVars', () => {
|
|
286
|
-
it('returns
|
|
286
|
+
it('returns correct tokens for each theme', () => {
|
|
287
287
|
expect(getCanvasThemeVars('light')['--sb--canvas-bg']).toBe('#f6f8fa')
|
|
288
288
|
expect(getCanvasThemeVars('light')['--tc-bg-muted']).toBe('#f6f8fa')
|
|
289
|
-
expect(getCanvasThemeVars('dark')['--sb--canvas-bg']).toBe('#
|
|
290
|
-
expect(getCanvasThemeVars('dark')['--bgColor-muted']).toBe('#
|
|
291
|
-
expect(getCanvasThemeVars('dark')['--tc-bg-muted']).toBe('#
|
|
292
|
-
expect(getCanvasThemeVars('dark_dimmed')['--sb--canvas-bg']).toBe('#
|
|
293
|
-
expect(getCanvasThemeVars('dark_dimmed')['--bgColor-muted']).toBe('#
|
|
294
|
-
expect(getCanvasThemeVars('dark_dimmed')['--tc-bg-muted']).toBe('#
|
|
295
|
-
expect(getCanvasThemeVars('dark_dimmed')['--tc-dot-color']).toBe('rgba(
|
|
296
|
-
expect(getCanvasThemeVars('dark_dimmed')['--overlay-backdrop-bgColor']).toBe('rgba(
|
|
289
|
+
expect(getCanvasThemeVars('dark')['--sb--canvas-bg']).toBe('#151b23')
|
|
290
|
+
expect(getCanvasThemeVars('dark')['--bgColor-muted']).toBe('#151b23')
|
|
291
|
+
expect(getCanvasThemeVars('dark')['--tc-bg-muted']).toBe('#151b23')
|
|
292
|
+
expect(getCanvasThemeVars('dark_dimmed')['--sb--canvas-bg']).toBe('#262c36')
|
|
293
|
+
expect(getCanvasThemeVars('dark_dimmed')['--bgColor-muted']).toBe('#262c36')
|
|
294
|
+
expect(getCanvasThemeVars('dark_dimmed')['--tc-bg-muted']).toBe('#262c36')
|
|
295
|
+
expect(getCanvasThemeVars('dark_dimmed')['--tc-dot-color']).toBe('rgba(209, 215, 224, 0.18)')
|
|
296
|
+
expect(getCanvasThemeVars('dark_dimmed')['--overlay-backdrop-bgColor']).toBe('rgba(209, 215, 224, 0.18)')
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
it('returns distinct values for dark_high_contrast', () => {
|
|
300
|
+
const vars = getCanvasThemeVars('dark_high_contrast')
|
|
301
|
+
expect(vars['--bgColor-default']).toBe('#010409')
|
|
302
|
+
expect(vars['--borderColor-default']).toBe('#b7bdc8')
|
|
303
|
+
expect(vars['--fgColor-default']).toBe('#ffffff')
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('returns distinct values for dark_colorblind', () => {
|
|
307
|
+
const vars = getCanvasThemeVars('dark_colorblind')
|
|
308
|
+
expect(vars['--bgColor-default']).toBe('#0d1117')
|
|
309
|
+
expect(vars['--fgColor-muted']).toBe('#9198a1')
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('returns distinct values for light_colorblind', () => {
|
|
313
|
+
const vars = getCanvasThemeVars('light_colorblind')
|
|
314
|
+
expect(vars['--bgColor-default']).toBe('#ffffff')
|
|
315
|
+
expect(vars['--fgColor-muted']).toBe('#59636e')
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('falls back to light for unknown themes', () => {
|
|
319
|
+
expect(getCanvasThemeVars('unknown')).toEqual(getCanvasThemeVars('light'))
|
|
297
320
|
})
|
|
298
321
|
})
|
|
299
322
|
|
|
@@ -304,6 +327,11 @@ describe('getCanvasPrimerAttrs', () => {
|
|
|
304
327
|
'data-dark-theme': 'dark',
|
|
305
328
|
'data-light-theme': 'light',
|
|
306
329
|
})
|
|
330
|
+
expect(getCanvasPrimerAttrs('light_colorblind')).toEqual({
|
|
331
|
+
'data-color-mode': 'light',
|
|
332
|
+
'data-dark-theme': 'dark',
|
|
333
|
+
'data-light-theme': 'light_colorblind',
|
|
334
|
+
})
|
|
307
335
|
expect(getCanvasPrimerAttrs('dark')).toEqual({
|
|
308
336
|
'data-color-mode': 'dark',
|
|
309
337
|
'data-dark-theme': 'dark',
|
|
@@ -314,6 +342,16 @@ describe('getCanvasPrimerAttrs', () => {
|
|
|
314
342
|
'data-dark-theme': 'dark_dimmed',
|
|
315
343
|
'data-light-theme': 'light',
|
|
316
344
|
})
|
|
345
|
+
expect(getCanvasPrimerAttrs('dark_high_contrast')).toEqual({
|
|
346
|
+
'data-color-mode': 'dark',
|
|
347
|
+
'data-dark-theme': 'dark_high_contrast',
|
|
348
|
+
'data-light-theme': 'light',
|
|
349
|
+
})
|
|
350
|
+
expect(getCanvasPrimerAttrs('dark_colorblind')).toEqual({
|
|
351
|
+
'data-color-mode': 'dark',
|
|
352
|
+
'data-dark-theme': 'dark_colorblind',
|
|
353
|
+
'data-light-theme': 'light',
|
|
354
|
+
})
|
|
317
355
|
})
|
|
318
356
|
})
|
|
319
357
|
|
|
@@ -2035,7 +2035,7 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
|
|
|
2035
2035
|
return (
|
|
2036
2036
|
<>
|
|
2037
2037
|
<div className={styles.canvasTitle}>
|
|
2038
|
-
<a href={(import.meta.env?.BASE_URL || '/')} className={
|
|
2038
|
+
<a href={(import.meta.env?.BASE_URL || '/')} className={styles.canvasLogo} aria-label="Go to homepage">
|
|
2039
2039
|
<Icon name="iconoir/key-command" size={16} color="#fff" />
|
|
2040
2040
|
</a>
|
|
2041
2041
|
{siblingPages.length > 1 && (
|
|
@@ -1,74 +1,118 @@
|
|
|
1
1
|
export function getCanvasPrimerAttrs(theme) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
'data-color-mode': 'dark',
|
|
5
|
-
'data-dark-theme': 'dark_dimmed',
|
|
6
|
-
'data-light-theme': 'light',
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
if (String(theme || 'light').startsWith('dark')) {
|
|
2
|
+
const value = String(theme || 'light')
|
|
3
|
+
if (value.startsWith('dark')) {
|
|
10
4
|
return {
|
|
11
5
|
'data-color-mode': 'dark',
|
|
12
|
-
'data-dark-theme':
|
|
6
|
+
'data-dark-theme': value,
|
|
13
7
|
'data-light-theme': 'light',
|
|
14
8
|
}
|
|
15
9
|
}
|
|
16
10
|
return {
|
|
17
11
|
'data-color-mode': 'light',
|
|
18
12
|
'data-dark-theme': 'dark',
|
|
19
|
-
'data-light-theme': 'light',
|
|
13
|
+
'data-light-theme': value.startsWith('light') ? value : 'light',
|
|
20
14
|
}
|
|
21
15
|
}
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'--bgColor-neutral-muted': 'rgba(99, 110, 123, 0.3)',
|
|
31
|
-
'--bgColor-accent-emphasis': '#316dca',
|
|
32
|
-
'--tc-bg-muted': '#22272e',
|
|
33
|
-
'--tc-dot-color': 'rgba(205, 217, 229, 0.22)',
|
|
34
|
-
'--overlay-backdrop-bgColor': 'rgba(205, 217, 229, 0.22)',
|
|
35
|
-
'--fgColor-muted': '#768390',
|
|
36
|
-
'--fgColor-default': '#adbac7',
|
|
37
|
-
'--fgColor-onEmphasis': '#ffffff',
|
|
38
|
-
'--borderColor-default': '#444c56',
|
|
39
|
-
'--borderColor-muted': '#545d68',
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (value.startsWith('dark')) {
|
|
43
|
-
return {
|
|
44
|
-
'--sb--canvas-bg': '#161b22',
|
|
45
|
-
'--bgColor-default': '#161b22',
|
|
46
|
-
'--bgColor-muted': '#161b22',
|
|
47
|
-
'--bgColor-neutral-muted': 'rgba(110, 118, 129, 0.2)',
|
|
48
|
-
'--bgColor-accent-emphasis': '#2f81f7',
|
|
49
|
-
'--tc-bg-muted': '#161b22',
|
|
50
|
-
'--tc-dot-color': 'rgba(255, 255, 255, 0.1)',
|
|
51
|
-
'--overlay-backdrop-bgColor': 'rgba(255, 255, 255, 0.1)',
|
|
52
|
-
'--fgColor-muted': '#8b949e',
|
|
53
|
-
'--fgColor-default': '#e6edf3',
|
|
54
|
-
'--fgColor-onEmphasis': '#ffffff',
|
|
55
|
-
'--borderColor-default': '#30363d',
|
|
56
|
-
'--borderColor-muted': '#30363d',
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
17
|
+
/**
|
|
18
|
+
* Per-theme canvas CSS custom properties sourced from @primer/primitives.
|
|
19
|
+
* Each theme gets its own entry so high-contrast, colorblind, and dimmed
|
|
20
|
+
* variants all render with the correct background, dot, and text colors.
|
|
21
|
+
*/
|
|
22
|
+
const THEME_VARS = {
|
|
23
|
+
light: {
|
|
60
24
|
'--sb--canvas-bg': '#f6f8fa',
|
|
61
25
|
'--bgColor-default': '#ffffff',
|
|
26
|
+
'--bgColor-muted': '#f6f8fa',
|
|
27
|
+
'--bgColor-neutral-muted': '#818b981f',
|
|
28
|
+
'--bgColor-accent-emphasis': '#0969da',
|
|
62
29
|
'--tc-bg-muted': '#f6f8fa',
|
|
63
30
|
'--tc-dot-color': 'rgba(0, 0, 0, 0.08)',
|
|
64
31
|
'--overlay-backdrop-bgColor': 'rgba(0, 0, 0, 0.08)',
|
|
32
|
+
'--fgColor-muted': '#59636e',
|
|
33
|
+
'--fgColor-default': '#1f2328',
|
|
34
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
35
|
+
'--borderColor-default': '#d1d9e0',
|
|
36
|
+
'--borderColor-muted': '#d1d9e0b3',
|
|
37
|
+
},
|
|
38
|
+
light_colorblind: {
|
|
39
|
+
'--sb--canvas-bg': '#f6f8fa',
|
|
40
|
+
'--bgColor-default': '#ffffff',
|
|
65
41
|
'--bgColor-muted': '#f6f8fa',
|
|
66
|
-
'--bgColor-neutral-muted': '#
|
|
67
|
-
'--bgColor-accent-emphasis': '#
|
|
68
|
-
'--
|
|
42
|
+
'--bgColor-neutral-muted': '#818b981f',
|
|
43
|
+
'--bgColor-accent-emphasis': '#0969da',
|
|
44
|
+
'--tc-bg-muted': '#f6f8fa',
|
|
45
|
+
'--tc-dot-color': 'rgba(0, 0, 0, 0.08)',
|
|
46
|
+
'--overlay-backdrop-bgColor': 'rgba(0, 0, 0, 0.08)',
|
|
47
|
+
'--fgColor-muted': '#59636e',
|
|
69
48
|
'--fgColor-default': '#1f2328',
|
|
70
49
|
'--fgColor-onEmphasis': '#ffffff',
|
|
71
50
|
'--borderColor-default': '#d1d9e0',
|
|
72
|
-
'--borderColor-muted': '#
|
|
73
|
-
}
|
|
51
|
+
'--borderColor-muted': '#d1d9e0b3',
|
|
52
|
+
},
|
|
53
|
+
dark: {
|
|
54
|
+
'--sb--canvas-bg': '#151b23',
|
|
55
|
+
'--bgColor-default': '#0d1117',
|
|
56
|
+
'--bgColor-muted': '#151b23',
|
|
57
|
+
'--bgColor-neutral-muted': '#656c7633',
|
|
58
|
+
'--bgColor-accent-emphasis': '#1f6feb',
|
|
59
|
+
'--tc-bg-muted': '#151b23',
|
|
60
|
+
'--tc-dot-color': 'rgba(255, 255, 255, 0.1)',
|
|
61
|
+
'--overlay-backdrop-bgColor': 'rgba(255, 255, 255, 0.1)',
|
|
62
|
+
'--fgColor-muted': '#9198a1',
|
|
63
|
+
'--fgColor-default': '#f0f6fc',
|
|
64
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
65
|
+
'--borderColor-default': '#3d444d',
|
|
66
|
+
'--borderColor-muted': '#3d444db3',
|
|
67
|
+
},
|
|
68
|
+
dark_dimmed: {
|
|
69
|
+
'--sb--canvas-bg': '#262c36',
|
|
70
|
+
'--bgColor-default': '#212830',
|
|
71
|
+
'--bgColor-muted': '#262c36',
|
|
72
|
+
'--bgColor-neutral-muted': '#656c7633',
|
|
73
|
+
'--bgColor-accent-emphasis': '#316dca',
|
|
74
|
+
'--tc-bg-muted': '#262c36',
|
|
75
|
+
'--tc-dot-color': 'rgba(209, 215, 224, 0.18)',
|
|
76
|
+
'--overlay-backdrop-bgColor': 'rgba(209, 215, 224, 0.18)',
|
|
77
|
+
'--fgColor-muted': '#9198a1',
|
|
78
|
+
'--fgColor-default': '#d1d7e0',
|
|
79
|
+
'--fgColor-onEmphasis': '#f0f6fc',
|
|
80
|
+
'--borderColor-default': '#3d444d',
|
|
81
|
+
'--borderColor-muted': '#3d444db3',
|
|
82
|
+
},
|
|
83
|
+
dark_colorblind: {
|
|
84
|
+
'--sb--canvas-bg': '#151b23',
|
|
85
|
+
'--bgColor-default': '#0d1117',
|
|
86
|
+
'--bgColor-muted': '#151b23',
|
|
87
|
+
'--bgColor-neutral-muted': '#656c7633',
|
|
88
|
+
'--bgColor-accent-emphasis': '#1f6feb',
|
|
89
|
+
'--tc-bg-muted': '#151b23',
|
|
90
|
+
'--tc-dot-color': 'rgba(255, 255, 255, 0.1)',
|
|
91
|
+
'--overlay-backdrop-bgColor': 'rgba(255, 255, 255, 0.1)',
|
|
92
|
+
'--fgColor-muted': '#9198a1',
|
|
93
|
+
'--fgColor-default': '#f0f6fc',
|
|
94
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
95
|
+
'--borderColor-default': '#3d444d',
|
|
96
|
+
'--borderColor-muted': '#3d444db3',
|
|
97
|
+
},
|
|
98
|
+
dark_high_contrast: {
|
|
99
|
+
'--sb--canvas-bg': '#151b23',
|
|
100
|
+
'--bgColor-default': '#010409',
|
|
101
|
+
'--bgColor-muted': '#151b23',
|
|
102
|
+
'--bgColor-neutral-muted': '#212830',
|
|
103
|
+
'--bgColor-accent-emphasis': '#194fb1',
|
|
104
|
+
'--tc-bg-muted': '#151b23',
|
|
105
|
+
'--tc-dot-color': 'rgba(183, 189, 200, 0.25)',
|
|
106
|
+
'--overlay-backdrop-bgColor': 'rgba(183, 189, 200, 0.25)',
|
|
107
|
+
'--fgColor-muted': '#b7bdc8',
|
|
108
|
+
'--fgColor-default': '#ffffff',
|
|
109
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
110
|
+
'--borderColor-default': '#b7bdc8',
|
|
111
|
+
'--borderColor-muted': '#b7bdc8',
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getCanvasThemeVars(theme) {
|
|
116
|
+
const value = String(theme || 'light')
|
|
117
|
+
return THEME_VARS[value] || THEME_VARS.light
|
|
74
118
|
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resolve the effective canvas theme from
|
|
2
|
+
* Resolve the effective canvas theme from the core theme store.
|
|
3
3
|
* Respects the canvas-specific theme sync toggle.
|
|
4
4
|
*/
|
|
5
|
+
import { getTheme, getThemeSyncTargets } from '@dfosco/storyboard-core'
|
|
6
|
+
|
|
7
|
+
function resolveSystem() {
|
|
8
|
+
if (typeof window === 'undefined') return 'light'
|
|
9
|
+
return window.matchMedia?.('(prefers-color-scheme: dark)')?.matches ? 'dark' : 'light'
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
export function resolveCanvasTheme() {
|
|
6
|
-
|
|
7
|
-
let sync = { prototype: true, toolbar: false, codeBoxes: true, canvas: true }
|
|
8
|
-
try {
|
|
9
|
-
const rawSync = localStorage.getItem('sb-theme-sync')
|
|
10
|
-
if (rawSync) sync = { ...sync, ...JSON.parse(rawSync) }
|
|
11
|
-
} catch { /* ignore */ }
|
|
13
|
+
const sync = getThemeSyncTargets()
|
|
12
14
|
if (!sync.canvas) return 'light'
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
const stored = localStorage.getItem('sb-color-scheme') || 'system'
|
|
16
|
-
if (stored !== 'system') return stored
|
|
17
|
-
return typeof window.matchMedia === 'function' &&
|
|
18
|
-
window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
15
|
+
const theme = getTheme()
|
|
16
|
+
return theme === 'system' ? resolveSystem() : theme
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
/**
|
|
@@ -54,52 +52,97 @@ export function subscribeCanvasTheme({ anchorRef, onTheme }) {
|
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
'--bgColor-muted': '#2d333b',
|
|
63
|
-
'--bgColor-neutral-muted': 'rgba(99, 110, 123, 0.3)',
|
|
64
|
-
'--fgColor-default': '#adbac7',
|
|
65
|
-
'--fgColor-muted': '#768390',
|
|
66
|
-
'--fgColor-onEmphasis': '#ffffff',
|
|
67
|
-
'--borderColor-default': '#444c56',
|
|
68
|
-
'--borderColor-muted': '#545d68',
|
|
69
|
-
'--bgColor-accent-emphasis': '#316dca',
|
|
70
|
-
'--trigger-bg': '#2d333b',
|
|
71
|
-
'--trigger-bg-hover': '#373e47',
|
|
72
|
-
'--trigger-border': '#444c56',
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (value.startsWith('dark')) {
|
|
76
|
-
return {
|
|
77
|
-
'--bgColor-default': '#161b22',
|
|
78
|
-
'--bgColor-muted': '#21262d',
|
|
79
|
-
'--bgColor-neutral-muted': 'rgba(110, 118, 129, 0.2)',
|
|
80
|
-
'--fgColor-default': '#e6edf3',
|
|
81
|
-
'--fgColor-muted': '#8b949e',
|
|
82
|
-
'--fgColor-onEmphasis': '#ffffff',
|
|
83
|
-
'--borderColor-default': '#30363d',
|
|
84
|
-
'--borderColor-muted': '#30363d',
|
|
85
|
-
'--bgColor-accent-emphasis': '#2f81f7',
|
|
86
|
-
'--trigger-bg': '#21262d',
|
|
87
|
-
'--trigger-bg-hover': '#30363d',
|
|
88
|
-
'--trigger-border': '#30363d',
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return {
|
|
55
|
+
/**
|
|
56
|
+
* Per-theme embed chrome CSS custom properties sourced from @primer/primitives.
|
|
57
|
+
*/
|
|
58
|
+
const EMBED_CHROME_VARS = {
|
|
59
|
+
light: {
|
|
92
60
|
'--bgColor-default': '#ffffff',
|
|
93
61
|
'--bgColor-muted': '#f6f8fa',
|
|
94
|
-
'--bgColor-neutral-muted': '#
|
|
62
|
+
'--bgColor-neutral-muted': '#818b981f',
|
|
95
63
|
'--fgColor-default': '#1f2328',
|
|
96
|
-
'--fgColor-muted': '#
|
|
64
|
+
'--fgColor-muted': '#59636e',
|
|
97
65
|
'--fgColor-onEmphasis': '#ffffff',
|
|
98
|
-
'--borderColor-default': '#
|
|
99
|
-
'--borderColor-muted': '#
|
|
100
|
-
'--bgColor-accent-emphasis': '#
|
|
66
|
+
'--borderColor-default': '#d1d9e0',
|
|
67
|
+
'--borderColor-muted': '#d1d9e0b3',
|
|
68
|
+
'--bgColor-accent-emphasis': '#0969da',
|
|
101
69
|
'--trigger-bg': '#f6f8fa',
|
|
102
70
|
'--trigger-bg-hover': '#eaeef2',
|
|
103
|
-
'--trigger-border': '#
|
|
104
|
-
}
|
|
71
|
+
'--trigger-border': '#d1d9e0',
|
|
72
|
+
},
|
|
73
|
+
light_colorblind: {
|
|
74
|
+
'--bgColor-default': '#ffffff',
|
|
75
|
+
'--bgColor-muted': '#f6f8fa',
|
|
76
|
+
'--bgColor-neutral-muted': '#818b981f',
|
|
77
|
+
'--fgColor-default': '#1f2328',
|
|
78
|
+
'--fgColor-muted': '#59636e',
|
|
79
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
80
|
+
'--borderColor-default': '#d1d9e0',
|
|
81
|
+
'--borderColor-muted': '#d1d9e0b3',
|
|
82
|
+
'--bgColor-accent-emphasis': '#0969da',
|
|
83
|
+
'--trigger-bg': '#f6f8fa',
|
|
84
|
+
'--trigger-bg-hover': '#eaeef2',
|
|
85
|
+
'--trigger-border': '#d1d9e0',
|
|
86
|
+
},
|
|
87
|
+
dark: {
|
|
88
|
+
'--bgColor-default': '#0d1117',
|
|
89
|
+
'--bgColor-muted': '#151b23',
|
|
90
|
+
'--bgColor-neutral-muted': '#656c7633',
|
|
91
|
+
'--fgColor-default': '#f0f6fc',
|
|
92
|
+
'--fgColor-muted': '#9198a1',
|
|
93
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
94
|
+
'--borderColor-default': '#3d444d',
|
|
95
|
+
'--borderColor-muted': '#3d444db3',
|
|
96
|
+
'--bgColor-accent-emphasis': '#1f6feb',
|
|
97
|
+
'--trigger-bg': '#151b23',
|
|
98
|
+
'--trigger-bg-hover': '#212830',
|
|
99
|
+
'--trigger-border': '#3d444d',
|
|
100
|
+
},
|
|
101
|
+
dark_dimmed: {
|
|
102
|
+
'--bgColor-default': '#212830',
|
|
103
|
+
'--bgColor-muted': '#262c36',
|
|
104
|
+
'--bgColor-neutral-muted': '#656c7633',
|
|
105
|
+
'--fgColor-default': '#d1d7e0',
|
|
106
|
+
'--fgColor-muted': '#9198a1',
|
|
107
|
+
'--fgColor-onEmphasis': '#f0f6fc',
|
|
108
|
+
'--borderColor-default': '#3d444d',
|
|
109
|
+
'--borderColor-muted': '#3d444db3',
|
|
110
|
+
'--bgColor-accent-emphasis': '#316dca',
|
|
111
|
+
'--trigger-bg': '#262c36',
|
|
112
|
+
'--trigger-bg-hover': '#2d333b',
|
|
113
|
+
'--trigger-border': '#3d444d',
|
|
114
|
+
},
|
|
115
|
+
dark_colorblind: {
|
|
116
|
+
'--bgColor-default': '#0d1117',
|
|
117
|
+
'--bgColor-muted': '#151b23',
|
|
118
|
+
'--bgColor-neutral-muted': '#656c7633',
|
|
119
|
+
'--fgColor-default': '#f0f6fc',
|
|
120
|
+
'--fgColor-muted': '#9198a1',
|
|
121
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
122
|
+
'--borderColor-default': '#3d444d',
|
|
123
|
+
'--borderColor-muted': '#3d444db3',
|
|
124
|
+
'--bgColor-accent-emphasis': '#1f6feb',
|
|
125
|
+
'--trigger-bg': '#151b23',
|
|
126
|
+
'--trigger-bg-hover': '#212830',
|
|
127
|
+
'--trigger-border': '#3d444d',
|
|
128
|
+
},
|
|
129
|
+
dark_high_contrast: {
|
|
130
|
+
'--bgColor-default': '#010409',
|
|
131
|
+
'--bgColor-muted': '#151b23',
|
|
132
|
+
'--bgColor-neutral-muted': '#212830',
|
|
133
|
+
'--fgColor-default': '#ffffff',
|
|
134
|
+
'--fgColor-muted': '#b7bdc8',
|
|
135
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
136
|
+
'--borderColor-default': '#b7bdc8',
|
|
137
|
+
'--borderColor-muted': '#b7bdc8',
|
|
138
|
+
'--bgColor-accent-emphasis': '#194fb1',
|
|
139
|
+
'--trigger-bg': '#151b23',
|
|
140
|
+
'--trigger-bg-hover': '#212830',
|
|
141
|
+
'--trigger-border': '#b7bdc8',
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function getEmbedChromeVars(theme) {
|
|
146
|
+
const value = String(theme || 'light')
|
|
147
|
+
return EMBED_CHROME_VARS[value] || EMBED_CHROME_VARS.light
|
|
105
148
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useThemeState — React hook for the global storyboard theme.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to the core themeStore via useSyncExternalStore so React
|
|
5
|
+
* components re-render whenever the theme changes (user action, system
|
|
6
|
+
* preference toggle, or sync-target change).
|
|
7
|
+
*
|
|
8
|
+
* Returns { theme, resolved } where `theme` may be "system" and
|
|
9
|
+
* `resolved` is always a concrete Primer theme name.
|
|
10
|
+
*
|
|
11
|
+
* useThemeSyncTargets — React hook for which UI surfaces follow the theme.
|
|
12
|
+
* Returns { prototype, toolbar, codeBoxes, canvas }.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useSyncExternalStore } from 'react'
|
|
16
|
+
import {
|
|
17
|
+
themeState,
|
|
18
|
+
themeSyncState,
|
|
19
|
+
} from '@dfosco/storyboard-core'
|
|
20
|
+
|
|
21
|
+
// --- useThemeState ----------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function subscribeTheme(cb) {
|
|
24
|
+
return themeState.subscribe(cb)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let _themeSnapshot = null
|
|
28
|
+
themeState.subscribe((s) => { _themeSnapshot = s })
|
|
29
|
+
|
|
30
|
+
function getThemeSnapshot() {
|
|
31
|
+
return _themeSnapshot
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to the global storyboard theme.
|
|
36
|
+
* @returns {{ theme: string, resolved: string }}
|
|
37
|
+
*/
|
|
38
|
+
export function useThemeState() {
|
|
39
|
+
return useSyncExternalStore(subscribeTheme, getThemeSnapshot, getThemeSnapshot)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- useThemeSyncTargets ----------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function subscribeSyncTargets(cb) {
|
|
45
|
+
return themeSyncState.subscribe(cb)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let _syncSnapshot = null
|
|
49
|
+
themeSyncState.subscribe((s) => { _syncSnapshot = s })
|
|
50
|
+
|
|
51
|
+
function getSyncSnapshot() {
|
|
52
|
+
return _syncSnapshot
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Subscribe to which UI surfaces follow the global theme.
|
|
57
|
+
* @returns {{ prototype: boolean, toolbar: boolean, codeBoxes: boolean, canvas: boolean }}
|
|
58
|
+
*/
|
|
59
|
+
export function useThemeSyncTargets() {
|
|
60
|
+
return useSyncExternalStore(subscribeSyncTargets, getSyncSnapshot, getSyncSnapshot)
|
|
61
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react'
|
|
2
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
3
|
+
import { setTheme, setThemeSyncTarget } from '@dfosco/storyboard-core'
|
|
4
|
+
import { useThemeState, useThemeSyncTargets } from './useThemeState.js'
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
localStorage.clear()
|
|
8
|
+
// Reset to defaults
|
|
9
|
+
setTheme('system')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('useThemeState', () => {
|
|
13
|
+
it('returns { theme, resolved }', () => {
|
|
14
|
+
const { result } = renderHook(() => useThemeState())
|
|
15
|
+
expect(result.current).toHaveProperty('theme')
|
|
16
|
+
expect(result.current).toHaveProperty('resolved')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('defaults to system theme', () => {
|
|
20
|
+
const { result } = renderHook(() => useThemeState())
|
|
21
|
+
expect(result.current.theme).toBe('system')
|
|
22
|
+
// resolved should be 'light' or 'dark' depending on matchMedia mock
|
|
23
|
+
expect(['light', 'dark']).toContain(result.current.resolved)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('updates when setTheme is called', () => {
|
|
27
|
+
const { result } = renderHook(() => useThemeState())
|
|
28
|
+
|
|
29
|
+
act(() => {
|
|
30
|
+
setTheme('dark_dimmed')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(result.current.theme).toBe('dark_dimmed')
|
|
34
|
+
expect(result.current.resolved).toBe('dark_dimmed')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('reverts to system when set back', () => {
|
|
38
|
+
const { result } = renderHook(() => useThemeState())
|
|
39
|
+
|
|
40
|
+
act(() => { setTheme('dark') })
|
|
41
|
+
expect(result.current.theme).toBe('dark')
|
|
42
|
+
|
|
43
|
+
act(() => { setTheme('system') })
|
|
44
|
+
expect(result.current.theme).toBe('system')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('useThemeSyncTargets', () => {
|
|
49
|
+
it('returns default sync targets', () => {
|
|
50
|
+
const { result } = renderHook(() => useThemeSyncTargets())
|
|
51
|
+
expect(result.current.prototype).toBe(true)
|
|
52
|
+
expect(result.current.toolbar).toBe(false)
|
|
53
|
+
expect(result.current.codeBoxes).toBe(true)
|
|
54
|
+
expect(result.current.canvas).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('updates when setThemeSyncTarget is called', () => {
|
|
58
|
+
const { result } = renderHook(() => useThemeSyncTargets())
|
|
59
|
+
|
|
60
|
+
act(() => {
|
|
61
|
+
setThemeSyncTarget('toolbar', true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(result.current.toolbar).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
})
|
package/src/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { useHideMode } from './hooks/useHideMode.js'
|
|
|
24
24
|
export { useUndoRedo } from './hooks/useUndoRedo.js'
|
|
25
25
|
export { useFeatureFlag } from './hooks/useFeatureFlag.js'
|
|
26
26
|
export { useMode } from './hooks/useMode.js'
|
|
27
|
+
export { useThemeState, useThemeSyncTargets } from './hooks/useThemeState.js'
|
|
27
28
|
|
|
28
29
|
// React Router integration
|
|
29
30
|
export { installHashPreserver } from './hashPreserver.js'
|
package/src/vite/data-plugin.js
CHANGED
|
@@ -731,6 +731,12 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
731
731
|
initCalls.push(`initUIConfig(${JSON.stringify(config.ui)})`)
|
|
732
732
|
}
|
|
733
733
|
|
|
734
|
+
// Customer mode config from storyboard.config.json
|
|
735
|
+
if (config?.customerMode) {
|
|
736
|
+
imports.push(`import { initCustomerModeConfig } from '@dfosco/storyboard-core'`)
|
|
737
|
+
initCalls.push(`initCustomerModeConfig(${JSON.stringify(config.customerMode)})`)
|
|
738
|
+
}
|
|
739
|
+
|
|
734
740
|
// Log info when multiple flows target the same route
|
|
735
741
|
const routeGroups = {}
|
|
736
742
|
for (const [name, { route, isDefault }] of Object.entries(resolvedFlowRoutes)) {
|