@pandacss/language-server 0.0.0-dev-20230607205019

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.
@@ -0,0 +1,252 @@
1
+ import { Builder } from '@pandacss/node'
2
+ import { type Connection, DidChangeConfigurationNotification, TextDocuments } from 'vscode-languageserver'
3
+ import { TextDocument } from 'vscode-languageserver-textdocument'
4
+ import { serverCapabilities } from './capabilities'
5
+ import { type PandaVSCodeSettings, defaultSettings, getFlattenedSettings } from '@pandacss/extension-shared'
6
+ import glob from 'fast-glob'
7
+ import { uriToPath } from './uri-to-path'
8
+ import { BuilderResolver } from './builder-resolver'
9
+
10
+ const builderResolver = new BuilderResolver()
11
+
12
+ const ref = {
13
+ settings: null as PandaVSCodeSettings | null,
14
+ /**
15
+ * current builder's context, used by most features as we can only be in one context at a time
16
+ * depending on the active document
17
+ */
18
+ context: null as unknown as Builder['context'],
19
+ synchronizing: false as Promise<void> | false,
20
+ //
21
+ activeDocumentFilepath: '',
22
+ }
23
+
24
+ let hasConfigurationCapability = false
25
+ let hasWorkspaceFolderCapability = false
26
+
27
+ /**
28
+ * Setup builder
29
+ * - panda.config detection & context loading
30
+ * - reload on panda.config change
31
+ * - make the builder.setup promise shared so it can be awaited by multiple features
32
+ */
33
+ export function setupBuilder(
34
+ connection: Connection,
35
+ documents: TextDocuments<TextDocument>,
36
+ {
37
+ onDidChangeConfiguration,
38
+ onReady,
39
+ }: {
40
+ onReady: () => void
41
+ onDidChangeConfiguration: (settings: PandaVSCodeSettings) => void
42
+ },
43
+ ) {
44
+ builderResolver.onSetup(({ configPath }) => {
45
+ const builder = builderResolver.get(configPath)
46
+ if (!builder) return
47
+
48
+ const ctx = builder.context
49
+ if (!ctx) return
50
+
51
+ const tokenNames = Array.from(new Set(ctx.tokens.allTokens.map((token) => token.path.slice(1).join('.'))))
52
+ connection.sendNotification('$/panda-token-names', { configPath, tokenNames })
53
+ })
54
+
55
+ async function setupWorkspaceBuilders(rootPath: string) {
56
+ console.log('🐼 Setup workspace builders...')
57
+ const configPathList = await glob(`${rootPath}/**/panda.config.{ts,js,cjs,mjs}`, {
58
+ cwd: rootPath,
59
+ onlyFiles: true,
60
+ absolute: true,
61
+ ignore: ['**/node_modules/**'],
62
+ })
63
+
64
+ await Promise.all(
65
+ configPathList.map(async (configPath) => {
66
+ try {
67
+ console.log('💼 Config setup at:', configPath)
68
+ await builderResolver.create(configPath).setup(configPath)
69
+ console.log('✅ Config setup done:', configPath)
70
+ } catch (err) {
71
+ // Ignore
72
+ console.log('❌ Config setup failed!', configPath, err)
73
+ }
74
+ }),
75
+ )
76
+ console.log('🐼 Workspaces builders ready !')
77
+
78
+ if (configPathList.length === 1) {
79
+ return builderResolver.isContextSynchronizing(configPathList[0])
80
+ }
81
+ }
82
+
83
+ async function loadPandaContext(uriOrFilepath: string) {
84
+ const filepath = uriToPath(uriOrFilepath) ?? uriOrFilepath
85
+
86
+ try {
87
+ console.log('🚧 Loading context for:', filepath)
88
+ ref.synchronizing = builderResolver.setup(filepath)
89
+ console.log('✅ Loading context done:', filepath)
90
+ } catch (err) {
91
+ // Ignore
92
+ ref.synchronizing = false
93
+ console.log('❌ Loading context failed!', err)
94
+ return
95
+ }
96
+
97
+ ref.synchronizing = false
98
+
99
+ const builder = builderResolver.get(filepath)
100
+ if (!builder || !builder.context) return
101
+
102
+ ref.context = builder.context
103
+
104
+ return ref.context
105
+ }
106
+
107
+ function getClosestPandaContext(uri: string) {
108
+ const filepath = uriToPath(uri) ?? uri
109
+
110
+ const builder = builderResolver.get(filepath)
111
+ if (!builder || !builder.context) return
112
+
113
+ ref.context = builder.context
114
+
115
+ return ref.context
116
+ }
117
+
118
+ const getFreshPandaSettings = async () => {
119
+ return getFlattenedSettings((await connection.workspace.getConfiguration('panda')) ?? defaultSettings)
120
+ }
121
+
122
+ /**
123
+ * Resolve current extension settings
124
+ */
125
+ async function getPandaSettings(): Promise<PandaVSCodeSettings>
126
+ async function getPandaSettings<Key extends keyof PandaVSCodeSettings>(key: Key): Promise<PandaVSCodeSettings[Key]>
127
+ async function getPandaSettings<Key extends keyof PandaVSCodeSettings>(key?: Key) {
128
+ const getter = (settings: PandaVSCodeSettings) => {
129
+ return key ? settings[key] : settings
130
+ }
131
+
132
+ if (!hasConfigurationCapability) {
133
+ return getter(defaultSettings)
134
+ }
135
+
136
+ if (!ref.settings) {
137
+ ref.settings = await getFreshPandaSettings()
138
+ }
139
+
140
+ return getter(ref.settings ?? defaultSettings)
141
+ }
142
+
143
+ connection.onInitialize((params) => {
144
+ connection.console.log('🤖 Starting PandaCss LSP...')
145
+
146
+ const capabilities = params.capabilities
147
+
148
+ const { activeDocumentFilepath } = params.initializationOptions as { activeDocumentFilepath: string | undefined }
149
+ if (activeDocumentFilepath) {
150
+ ref.activeDocumentFilepath = activeDocumentFilepath
151
+ console.log('📄 Init Active document:', activeDocumentFilepath)
152
+ }
153
+
154
+ // Check context support
155
+ hasConfigurationCapability = !!(capabilities.workspace && !!capabilities.workspace.configuration)
156
+ hasWorkspaceFolderCapability = !!(capabilities.workspace && !!capabilities.workspace.workspaceFolders)
157
+
158
+ connection.onInitialized(async () => {
159
+ connection.console.log('⚡ Connection initialized!')
160
+
161
+ if (hasConfigurationCapability) {
162
+ // Register for all configuration changes.
163
+ connection.client.register(DidChangeConfigurationNotification.type, undefined)
164
+
165
+ ref.settings = await getFreshPandaSettings()
166
+ onDidChangeConfiguration(ref.settings)
167
+ }
168
+
169
+ if (hasWorkspaceFolderCapability) {
170
+ connection.workspace.onDidChangeWorkspaceFolders((_event) =>
171
+ connection.console.log('Workspace folder change event received.'),
172
+ )
173
+ }
174
+
175
+ const workspaceFolders = await connection.workspace.getWorkspaceFolders()
176
+ const validFolders = workspaceFolders?.map((folder) => uriToPath(folder.uri) || '').filter((path) => !!path)
177
+
178
+ console.log('📁 Workspace folders:', validFolders)
179
+ await Promise.all(validFolders?.map((folder) => setupWorkspaceBuilders(folder)) ?? [])
180
+
181
+ onReady()
182
+ connection.sendNotification('$/panda-lsp-ready')
183
+
184
+ if (activeDocumentFilepath) {
185
+ const ctx = getClosestPandaContext(activeDocumentFilepath)
186
+
187
+ if (ctx) {
188
+ connection.console.log(`🐼 Found panda context! ✅ at ${ctx.path}`)
189
+ }
190
+ }
191
+ })
192
+
193
+ return { capabilities: serverCapabilities }
194
+ })
195
+
196
+ connection.onNotification('$/panda-active-document-changed', (params) => {
197
+ console.log('📄 Active document:', ref.activeDocumentFilepath)
198
+ ref.activeDocumentFilepath = params.activeDocumentFilepath
199
+
200
+ const configPath = builderResolver.findConfigDirpath(ref.activeDocumentFilepath, (_, configPath) => configPath)
201
+ if (!configPath) return
202
+
203
+ connection.sendNotification('$/panda-doc-config-path', {
204
+ activeDocumentFilepath: ref.activeDocumentFilepath,
205
+ configPath,
206
+ })
207
+ })
208
+
209
+ connection.onRequest('$/get-config-path', ({ activeDocumentFilepath }: { activeDocumentFilepath: string }) => {
210
+ activeDocumentFilepath ??= ref.activeDocumentFilepath
211
+ if (!activeDocumentFilepath) return
212
+
213
+ return builderResolver.findConfigDirpath(activeDocumentFilepath, (_, configPath) => {
214
+ console.log('config path', configPath)
215
+ return configPath
216
+ })
217
+ })
218
+
219
+ connection.onDidChangeConfiguration(async (_change) => {
220
+ connection.console.log('⌛ onDidChangeConfiguration')
221
+
222
+ if (hasConfigurationCapability) {
223
+ ref.settings = await getFreshPandaSettings()
224
+ console.log('🐼 Settings changed!', ref.settings)
225
+ onDidChangeConfiguration(ref.settings)
226
+ }
227
+ })
228
+
229
+ connection.onDidChangeWatchedFiles(async ({ changes }) => {
230
+ changes.forEach(async (fileEvent) => {
231
+ const filepath = uriToPath(fileEvent.uri) ?? fileEvent.uri
232
+ connection.console.log('🔃 Reloading panda context for:' + filepath)
233
+ await builderResolver.setup(filepath)
234
+ })
235
+ })
236
+
237
+ documents.listen(connection)
238
+ connection.listen()
239
+
240
+ return {
241
+ getPandaSettings,
242
+ loadPandaContext,
243
+ getContext() {
244
+ return ref.context
245
+ },
246
+ isSynchronizing() {
247
+ return ref.synchronizing
248
+ },
249
+ }
250
+ }
251
+
252
+ export type PandaExtensionSetup = ReturnType<typeof setupBuilder>
@@ -0,0 +1,18 @@
1
+ import { type ColorPresentationParams } from 'vscode-languageserver'
2
+ import { parseToRgba } from 'color2k'
3
+
4
+ export const color2kToVsCodeColor = (value: string) => {
5
+ try {
6
+ const [red, green, blue, alpha] = parseToRgba(value)
7
+
8
+ const color = {
9
+ red: red / 255,
10
+ green: green / 255,
11
+ blue: blue / 255,
12
+ alpha,
13
+ }
14
+ return color as ColorPresentationParams['color']
15
+ } catch (e) {
16
+ return
17
+ }
18
+ }
@@ -0,0 +1,3 @@
1
+ export const onError = (...args: any[]) => {
2
+ console.error(args)
3
+ }
@@ -0,0 +1,43 @@
1
+ import { type Token } from '@pandacss/token-dictionary'
2
+
3
+ type TokenFnMatch = { token: Token; index: number }
4
+ const tokenRegex = /token\(([^)]+)\)/g
5
+
6
+ const matchToken = (str: string, callback: (tokenPath: string, match: RegExpExecArray) => void) => {
7
+ let match: RegExpExecArray | null
8
+
9
+ while ((match = tokenRegex.exec(str)) != null) {
10
+ match[1]
11
+ .split(',')
12
+ .map((s) => s.trim())
13
+ .filter(Boolean)
14
+ .forEach((str) => callback(str, match!))
15
+ }
16
+ }
17
+
18
+ /** @see packages/core/src/plugins/expand-token-fn.ts */
19
+ export const expandTokenFn = (str: string, fn: (tokenName: string) => Token | undefined) => {
20
+ if (!str.includes('token(')) return []
21
+
22
+ const tokens = [] as TokenFnMatch[]
23
+ matchToken(str, (tokenPath, match) => {
24
+ const token = fn(tokenPath)
25
+ if (token) {
26
+ tokens.push({ token, index: match.index })
27
+ }
28
+ })
29
+
30
+ return tokens
31
+ }
32
+
33
+ export const extractTokenPaths = (str: string) => {
34
+ if (!str.includes('token(')) return []
35
+
36
+ const paths = [] as string[]
37
+
38
+ matchToken(str, (tokenPath) => {
39
+ paths.push(tokenPath)
40
+ })
41
+
42
+ return paths
43
+ }
@@ -0,0 +1,28 @@
1
+ import { toCss } from '@pandacss/core'
2
+ import postcss from 'postcss'
3
+ import { type PandaContext } from '@pandacss/node'
4
+
5
+ /** @see packages/generator/src/artifacts/css/keyframe-css.ts */
6
+ export function generateKeyframeCss(ctx: PandaContext, keyframeName: string) {
7
+ const { keyframes = {} } = ctx.config.theme ?? {}
8
+ const root = postcss.root()
9
+
10
+ const keyframe = keyframes[keyframeName]
11
+ if (!keyframe) return ''
12
+
13
+ root.append(
14
+ postcss.atRule({
15
+ name: 'keyframes',
16
+ params: keyframeName,
17
+ nodes: toCss(keyframe).root.nodes,
18
+ }),
19
+ )
20
+
21
+ const rule = postcss.atRule({
22
+ name: 'layer',
23
+ params: 'tokens',
24
+ nodes: root.nodes,
25
+ })
26
+
27
+ return rule.toString()
28
+ }
@@ -0,0 +1,216 @@
1
+ // borrow from https://github.com/princejwesley/is-css-color/blob/master/index.js
2
+ 'use strict'
3
+
4
+ // every string I match against are lowercase
5
+ const HEX_PATTERN = /^#(?:[a-f0-9]{3})?(?:[a-f0-9]{3})$/
6
+
7
+ // css color names + initial + inherit + currentColor + transparent
8
+ const CSS_COLOR_NAMES = [
9
+ 'aliceblue',
10
+ 'antiquewhite',
11
+ 'aqua',
12
+ 'aquamarine',
13
+ 'azure',
14
+ 'beige',
15
+ 'bisque',
16
+ 'black',
17
+ 'blanchedalmond',
18
+ 'blue',
19
+ 'blueviolet',
20
+ 'brown',
21
+ 'burlywood',
22
+ 'cadetblue',
23
+ 'chartreuse',
24
+ 'chocolate',
25
+ 'coral',
26
+ 'cornflowerblue',
27
+ 'cornsilk',
28
+ 'crimson',
29
+ 'currentColor',
30
+ 'cyan',
31
+ 'darkblue',
32
+ 'darkcyan',
33
+ 'darkgoldenrod',
34
+ 'darkgray',
35
+ 'darkgreen',
36
+ 'darkgrey',
37
+ 'darkkhaki',
38
+ 'darkmagenta',
39
+ 'darkolivegreen',
40
+ 'darkorange',
41
+ 'darkorchid',
42
+ 'darkred',
43
+ 'darksalmon',
44
+ 'darkseagreen',
45
+ 'darkslateblue',
46
+ 'darkslategray',
47
+ 'darkslategrey',
48
+ 'darkturquoise',
49
+ 'darkviolet',
50
+ 'deeppink',
51
+ 'deepskyblue',
52
+ 'dimgray',
53
+ 'dimgrey',
54
+ 'dodgerblue',
55
+ 'firebrick',
56
+ 'floralwhite',
57
+ 'forestgreen',
58
+ 'fuchsia',
59
+ 'gainsboro',
60
+ 'ghostwhite',
61
+ 'gold',
62
+ 'goldenrod',
63
+ 'gray',
64
+ 'green',
65
+ 'greenyellow',
66
+ 'grey',
67
+ 'honeydew',
68
+ 'hotpink',
69
+ 'indianred',
70
+ 'indigo',
71
+ 'inherit',
72
+ 'initial',
73
+ 'ivory',
74
+ 'khaki',
75
+ 'lavender',
76
+ 'lavenderblush',
77
+ 'lawngreen',
78
+ 'lemonchiffon',
79
+ 'lightblue',
80
+ 'lightcoral',
81
+ 'lightcyan',
82
+ 'lightgoldenrodyellow',
83
+ 'lightgray',
84
+ 'lightgreen',
85
+ 'lightgrey',
86
+ 'lightpink',
87
+ 'lightsalmon',
88
+ 'lightseagreen',
89
+ 'lightskyblue',
90
+ 'lightslategray',
91
+ 'lightslategrey',
92
+ 'lightsteelblue',
93
+ 'lightyellow',
94
+ 'lime',
95
+ 'limegreen',
96
+ 'linen',
97
+ 'magenta',
98
+ 'maroon',
99
+ 'mediumaquamarine',
100
+ 'mediumblue',
101
+ 'mediumorchid',
102
+ 'mediumpurple',
103
+ 'mediumseagreen',
104
+ 'mediumslateblue',
105
+ 'mediumspringgreen',
106
+ 'mediumturquoise',
107
+ 'mediumvioletred',
108
+ 'midnightblue',
109
+ 'mintcream',
110
+ 'mistyrose',
111
+ 'moccasin',
112
+ 'navajowhite',
113
+ 'navy',
114
+ 'oldlace',
115
+ 'olive',
116
+ 'olivedrab',
117
+ 'orange',
118
+ 'orangered',
119
+ 'orchid',
120
+ 'palegoldenrod',
121
+ 'palegreen',
122
+ 'paleturquoise',
123
+ 'palevioletred',
124
+ 'papayawhip',
125
+ 'peachpuff',
126
+ 'peru',
127
+ 'pink',
128
+ 'plum',
129
+ 'powderblue',
130
+ 'purple',
131
+ 'rebeccapurple',
132
+ 'red',
133
+ 'rosybrown',
134
+ 'royalblue',
135
+ 'saddlebrown',
136
+ 'salmon',
137
+ 'sandybrown',
138
+ 'seagreen',
139
+ 'seashell',
140
+ 'sienna',
141
+ 'silver',
142
+ 'skyblue',
143
+ 'slateblue',
144
+ 'slategray',
145
+ 'slategrey',
146
+ 'snow',
147
+ 'springgreen',
148
+ 'steelblue',
149
+ 'tan',
150
+ 'teal',
151
+ 'thistle',
152
+ 'tomato',
153
+ 'transparent',
154
+ 'turquoise',
155
+ 'violet',
156
+ 'wheat',
157
+ 'white',
158
+ 'whitesmoke',
159
+ 'yellow',
160
+ 'yellowgreen',
161
+ ]
162
+
163
+ const PREFIX = '^(rgb|hsl)(a?)\\s*\\('
164
+ const VALUE = '\\s*([-+]?\\d+%?)\\s*'
165
+ const ALPHA = '(?:,\\s*([-+]?(?:(?:\\d+(?:.\\d+)?)|(?:.\\d+))\\s*))?'
166
+ const SUFFIX = '\\)$'
167
+ const RGB_HSL_PATTERN = new RegExp(PREFIX + VALUE + ',' + VALUE + ',' + VALUE + ALPHA + SUFFIX)
168
+
169
+ const NUM_TYPE = 1
170
+ const PERCENTAGE_TYPE = 2
171
+ const ERROR_TYPE = NUM_TYPE & PERCENTAGE_TYPE
172
+
173
+ export const isColor = (str: string) => {
174
+ function getColorType(token: string) {
175
+ return token.includes('%') ? PERCENTAGE_TYPE : NUM_TYPE
176
+ }
177
+
178
+ if (!str || typeof str !== 'string') {
179
+ return false
180
+ }
181
+
182
+ const color = str.replace(/^\s+|\s+$/g, '').toLocaleLowerCase()
183
+
184
+ // named colors or hex code
185
+ if (CSS_COLOR_NAMES.includes(color) || HEX_PATTERN.test(color)) {
186
+ return true
187
+ }
188
+
189
+ const result = color.match(RGB_HSL_PATTERN)
190
+ if (result) {
191
+ const flavor = result[1]
192
+ const alpha = result[2]
193
+ const rh = result[3]
194
+ const gs = result[4]
195
+ const bl = result[5]
196
+ const a = result[6]
197
+
198
+ // alpha test
199
+ if ((alpha === 'a' && !a) || (a && alpha === '')) {
200
+ return false
201
+ }
202
+
203
+ // hsl
204
+ if (flavor === 'hsl') {
205
+ if (getColorType(rh) !== NUM_TYPE) {
206
+ return false
207
+ }
208
+ return (getColorType(gs) & getColorType(bl)) === PERCENTAGE_TYPE
209
+ }
210
+
211
+ // rgb
212
+ return (getColorType(rh) & getColorType(gs) & getColorType(bl)) !== ERROR_TYPE
213
+ }
214
+
215
+ return false
216
+ }
@@ -0,0 +1,49 @@
1
+ // import fs from 'fs'
2
+ // import path from 'path'
3
+ import satori from 'satori'
4
+
5
+ // __dirname = extension/dist
6
+ // export const Roboto = fs.readFileSync(path.resolve(__dirname, '../Roboto-Medium.ttf'))
7
+ export const Roboto = Buffer.from('')
8
+
9
+ // https://og-playground.vercel.app/?share=bZBBT4QwEIX_SjPG7KWJuCqHZvWyMdGzJnvhUugUupaWtEVEwn-3BS9kd04z873My7wJKisQGByE-i4MIT6MGp-nKfWENKjqJjCyu8-y2x1dl2sNSoTmKhHKd5qPkUmNP1vGtarNe8DWR1yhCei2gnPvg5Lj0UZkkvM1Ucmrr9rZ3oij1dZF1Y2UcquR8cKH-kVGHvYX4PT_V55lC5vnwryk5g21tpScrNOiMIe7mErcAwXbBWWNBzbB8jmw_VNOYQ0IWP5IQWDZ18Ak1x4pYGvP6nPsUrhhWKZ4Jpm_tiUKYMH1OFMIvIyKJvkOyRXmPw
10
+ export const renderFontSizePreview = (
11
+ text: string,
12
+ fontSize: string,
13
+ options?: {
14
+ width: number
15
+ height: number
16
+ },
17
+ ) => {
18
+ return satori(
19
+ {
20
+ type: 'div',
21
+ key: null,
22
+ props: {
23
+ children: text,
24
+ style: {
25
+ height: '100%',
26
+ width: '100%',
27
+ display: 'flex',
28
+ alignItems: 'center',
29
+ justifyContent: 'center',
30
+ backgroundColor: '#fff',
31
+ fontSize: fontSize,
32
+ },
33
+ },
34
+ },
35
+ {
36
+ width: 264,
37
+ height: 64,
38
+ ...options,
39
+ fonts: [
40
+ {
41
+ name: 'Roboto',
42
+ data: Roboto,
43
+ weight: 400,
44
+ style: 'normal',
45
+ },
46
+ ],
47
+ },
48
+ )
49
+ }
@@ -0,0 +1,63 @@
1
+ import { type Dict } from '@pandacss/types'
2
+ import { type PandaContext } from '@pandacss/node'
3
+ import satori from 'satori'
4
+ import { type Token } from '@pandacss/token-dictionary'
5
+ import { svgToMarkdownLink } from './utils'
6
+ import { Roboto } from './render-font-size-preview'
7
+
8
+ // https://og-playground.vercel.app/?share=5ZTNTsMwDMdfJbLELdI2xHYIgwMfb4DEJZe2cdtAmlRJSqmqvjtNw8ZEGUzihMglsRP_9bMtp4fMCAQGWyFfuCbE-U7hVd-HMyElyqL0jHBYLZdnHGh0t1L4cuYV0tUq6YI_V_i69wfjTlrMvDQ63GZGNZXmEK6HgevrcNgBfEY4rhuVGVm920GKkEnsUG4uGANvEifdR3RYldSPu9TWB5l9U4o5Rlhpkj0X1jRa3BplbIgqLOKY8_5RpCVk8RvgL6BOy-Yk5FQ1eJR4uxiB_0XnOlTKtH-rdRbFj52L-yXXQMHUYTgdsB6m4QZ2vt5QiJDANhcUBKZNASxPlEMKWJkn-dDV4e_w7WSNMrnR_r5KUQDztsGBgk_S8UU5ldBYJWB4Aw
9
+ export const renderTokenColorPreview = async (ctx: PandaContext, token: Token) => {
10
+ const colors = [] as [tokenName: string, value: string][]
11
+
12
+ // Only bother displaying a preview for multiple colors
13
+ if (token.extensions.conditions) {
14
+ Object.entries(token.extensions.conditions).forEach(([conditionName, value]) => {
15
+ const [tokenRef] = ctx.tokens.getReferences(value)
16
+
17
+ colors.push([conditionName, tokenRef.value])
18
+ })
19
+ }
20
+ if (!colors.length) return
21
+
22
+ const svg = await satori(
23
+ createDiv(
24
+ {
25
+ display: 'flex',
26
+ flexDirection: 'column',
27
+ height: '100%',
28
+ width: '100%',
29
+ },
30
+ colors.map(([conditionName, backgroundColor]) =>
31
+ createDiv(
32
+ { height: '100%', flex: 1, padding: '4px', display: 'flex', justifyContent: 'center', backgroundColor },
33
+ createDiv({ height: '100%', padding: '4px', backgroundColor: 'white', color: 'black' }, conditionName),
34
+ ),
35
+ ),
36
+ ),
37
+ {
38
+ width: 256,
39
+ height: 28 * colors.length,
40
+ fonts: [
41
+ {
42
+ name: 'Roboto',
43
+ data: Roboto,
44
+ weight: 400,
45
+ style: 'normal',
46
+ },
47
+ ],
48
+ },
49
+ )
50
+
51
+ return svgToMarkdownLink(svg)
52
+ }
53
+
54
+ const createDiv = (style: Dict, children?: DivNode) => ({ type: 'div', key: null, props: { style, children } })
55
+ type DivLike = {
56
+ type: string
57
+ key: null
58
+ props: {
59
+ style: Dict
60
+ children: any
61
+ }
62
+ }
63
+ type DivNode = string | DivLike | DivLike[] | null