mppx 0.5.5 → 0.5.7
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/CHANGELOG.md +12 -0
- package/dist/Html.d.ts +11 -0
- package/dist/Html.d.ts.map +1 -0
- package/dist/Html.js +41 -0
- package/dist/Html.js.map +1 -0
- package/dist/server/Mppx.d.ts +1 -28
- package/dist/server/Mppx.d.ts.map +1 -1
- package/dist/server/Mppx.js +101 -30
- package/dist/server/Mppx.js.map +1 -1
- package/dist/server/Transport.d.ts.map +1 -1
- package/dist/server/Transport.js +18 -46
- package/dist/server/Transport.js.map +1 -1
- package/dist/server/internal/html/compose.main.gen.d.ts +2 -0
- package/dist/server/internal/html/compose.main.gen.d.ts.map +1 -0
- package/dist/server/internal/html/compose.main.gen.js +3 -0
- package/dist/server/internal/html/compose.main.gen.js.map +1 -0
- package/dist/server/internal/html/config.d.ts +50 -49
- package/dist/server/internal/html/config.d.ts.map +1 -1
- package/dist/server/internal/html/config.js +323 -117
- package/dist/server/internal/html/config.js.map +1 -1
- package/dist/server/internal/html/constants.d.ts +26 -0
- package/dist/server/internal/html/constants.d.ts.map +1 -0
- package/dist/server/internal/html/constants.js +26 -0
- package/dist/server/internal/html/constants.js.map +1 -0
- package/dist/server/internal/html/serviceWorker.client.d.ts +2 -0
- package/dist/server/internal/html/serviceWorker.client.d.ts.map +1 -0
- package/dist/server/internal/html/serviceWorker.client.js +26 -0
- package/dist/server/internal/html/serviceWorker.client.js.map +1 -0
- package/dist/stripe/server/Charge.d.ts +2 -4
- package/dist/stripe/server/Charge.d.ts.map +1 -1
- package/dist/stripe/server/Charge.js.map +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts +1 -1
- package/dist/stripe/server/internal/html.gen.d.ts.map +1 -1
- package/dist/stripe/server/internal/html.gen.js +1 -1
- package/dist/stripe/server/internal/html.gen.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts +1 -4
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js.map +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts +1 -1
- package/dist/tempo/server/internal/html.gen.d.ts.map +1 -1
- package/dist/tempo/server/internal/html.gen.js +1 -1
- package/dist/tempo/server/internal/html.gen.js.map +1 -1
- package/package.json +6 -1
- package/src/Html.ts +67 -0
- package/src/server/Mppx.test.ts +203 -0
- package/src/server/Mppx.ts +118 -3
- package/src/server/Transport.test.ts +5 -2
- package/src/server/Transport.ts +21 -54
- package/src/server/internal/html/compose.main.gen.ts +2 -0
- package/src/server/internal/html/compose.main.ts +88 -0
- package/src/server/internal/html/config.ts +427 -177
- package/src/server/internal/html/constants.ts +28 -0
- package/src/server/internal/html/serviceWorker.client.ts +2 -2
- package/src/server/internal/html/tsconfig.compose.json +8 -0
- package/src/stripe/server/Charge.ts +2 -4
- package/src/stripe/server/internal/html/main.ts +44 -53
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/server/Charge.ts +1 -7
- package/src/tempo/server/internal/html/main.ts +26 -28
- package/src/tempo/server/internal/html.gen.ts +1 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { Json } from 'ox'
|
|
2
|
+
|
|
1
3
|
import type * as Challenge from '../../../Challenge.js'
|
|
2
4
|
import type * as Method from '../../../Method.js'
|
|
5
|
+
import { tabScript } from './compose.main.gen.js'
|
|
3
6
|
|
|
4
7
|
export type Options = {
|
|
5
8
|
config: Record<string, unknown>
|
|
@@ -13,6 +16,9 @@ export type Data<
|
|
|
13
16
|
method extends Method.Method = Method.Method,
|
|
14
17
|
config extends Record<string, unknown> = {},
|
|
15
18
|
> = {
|
|
19
|
+
label: string
|
|
20
|
+
rootId: string
|
|
21
|
+
formattedAmount: string
|
|
16
22
|
config: config
|
|
17
23
|
challenge: Challenge.FromMethods<[method]>
|
|
18
24
|
text: { [k in keyof Text]-?: NonNullable<Text[k]> }
|
|
@@ -21,45 +27,15 @@ export type Data<
|
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
export const errorId = 'root_error'
|
|
27
|
-
|
|
28
|
-
export const rootId = 'root'
|
|
30
|
+
export { attrs, classNames, ids, params } from './constants.js'
|
|
31
|
+
import { attrs, classNames, ids } from './constants.js'
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
header: 'mppx-header',
|
|
35
|
-
logo: 'mppx-logo',
|
|
36
|
-
logoColorScheme: (colorScheme: string) =>
|
|
37
|
-
colorScheme === 'dark' || colorScheme === 'light'
|
|
38
|
-
? `${classNames.logo}--${colorScheme}`
|
|
39
|
-
: undefined,
|
|
40
|
-
summary: 'mppx-summary',
|
|
41
|
-
summaryAmount: 'mppx-summary-amount',
|
|
42
|
-
summaryDescription: 'mppx-summary-description',
|
|
43
|
-
summaryExpires: 'mppx-summary-expires',
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function sanitize(str: string): string {
|
|
47
|
-
return str
|
|
48
|
-
.replace(/&/g, '&')
|
|
49
|
-
.replace(/</g, '<')
|
|
50
|
-
.replace(/>/g, '>')
|
|
51
|
-
.replace(/"/g, '"')
|
|
52
|
-
.replace(/'/g, ''')
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function sanitizeRecord<type extends Record<string, string>>(record: type): type {
|
|
56
|
-
return Object.fromEntries(
|
|
57
|
-
Object.entries(record).map(([key, value]) => [key, sanitize(value)]),
|
|
58
|
-
) as type
|
|
33
|
+
function logoColorScheme(colorScheme: string) {
|
|
34
|
+
return colorScheme === 'dark' || colorScheme === 'light'
|
|
35
|
+
? `${classNames.logo}--${colorScheme}`
|
|
36
|
+
: undefined
|
|
59
37
|
}
|
|
60
38
|
|
|
61
|
-
export const html = String.raw
|
|
62
|
-
|
|
63
39
|
class CssVar {
|
|
64
40
|
readonly name: string
|
|
65
41
|
constructor(token: string) {
|
|
@@ -85,17 +61,241 @@ export const vars = {
|
|
|
85
61
|
spacingUnit: new CssVar('spacing-unit'),
|
|
86
62
|
} as const
|
|
87
63
|
|
|
88
|
-
export
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
64
|
+
export const defaultText = {
|
|
65
|
+
expires: 'Expires at',
|
|
66
|
+
pay: 'Pay',
|
|
67
|
+
paymentRequired: 'Payment Required',
|
|
68
|
+
title: 'Payment Required',
|
|
69
|
+
} as const satisfies Required<Text>
|
|
70
|
+
|
|
71
|
+
export type Text = {
|
|
72
|
+
/** Prefix for the expiry line. @default 'Expires at' */
|
|
73
|
+
expires?: string | undefined
|
|
74
|
+
/** Pay button label. @default 'Pay' */
|
|
75
|
+
pay?: string | undefined
|
|
76
|
+
/** Badge label. @default 'Payment Required' */
|
|
77
|
+
paymentRequired?: string | undefined
|
|
78
|
+
/** Page title. @default text.paymentRequired */
|
|
79
|
+
title?: string | undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const defaultTheme = {
|
|
83
|
+
colorScheme: 'light dark',
|
|
84
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
85
|
+
fontSizeBase: '16px',
|
|
86
|
+
radius: '6px',
|
|
87
|
+
spacingUnit: '2px',
|
|
88
|
+
|
|
89
|
+
accent: ['#171717', '#ededed'],
|
|
90
|
+
background: ['#ffffff', '#0a0a0a'],
|
|
91
|
+
border: ['#e5e5e5', '#2e2e2e'],
|
|
92
|
+
foreground: ['#0a0a0a', '#ededed'],
|
|
93
|
+
muted: ['#666666', '#a1a1a1'],
|
|
94
|
+
negative: ['#e5484d', '#e5484d'],
|
|
95
|
+
positive: ['#30a46c', '#30a46c'],
|
|
96
|
+
surface: ['#f5f5f5', '#1a1a1a'],
|
|
97
|
+
} as const satisfies Required<Omit<Theme, 'favicon' | 'fontUrl' | 'logo'>>
|
|
98
|
+
|
|
99
|
+
export type Theme = {
|
|
100
|
+
/** Color scheme. @default 'light dark' */
|
|
101
|
+
colorScheme?: 'light' | 'dark' | 'light dark' | undefined
|
|
102
|
+
/** Font family. @default 'system-ui, -apple-system, sans-serif' */
|
|
103
|
+
fontFamily?: string | undefined
|
|
104
|
+
/** Base font size. @default '16px' */
|
|
105
|
+
fontSizeBase?: string | undefined
|
|
106
|
+
/** Font URL to inject (e.g. Google Fonts `<link>`). */
|
|
107
|
+
fontUrl?: string | undefined
|
|
108
|
+
/** Favicon URL. Light/dark variants supported. Falls back to host's favicon via Google S2 service. */
|
|
109
|
+
favicon?: string | { light: string; dark: string } | undefined
|
|
110
|
+
/** Logo URL shown in header. Light/dark variants supported. */
|
|
111
|
+
logo?: string | { light: string; dark: string } | undefined
|
|
112
|
+
/** Border radius. @default '6px' */
|
|
113
|
+
radius?: string | undefined
|
|
114
|
+
/** The base spacing unit that all other spacing is derived from. Increase or decrease this value to make your layout more or less spacious. @default '2px' */
|
|
115
|
+
spacingUnit?: string | undefined
|
|
116
|
+
|
|
117
|
+
/** Accent color (buttons, links). @default ['#171717', '#ededed'] */
|
|
118
|
+
accent?: LightDark | undefined
|
|
119
|
+
/** Page background. @default ['#ffffff', '#0a0a0a'] */
|
|
120
|
+
background?: LightDark | undefined
|
|
121
|
+
/** Border color. @default ['#e5e5e5', '#2e2e2e'] */
|
|
122
|
+
border?: LightDark | undefined
|
|
123
|
+
/** Primary text/content color. @default ['#0a0a0a', '#ededed'] */
|
|
124
|
+
foreground?: LightDark | undefined
|
|
125
|
+
/** Secondary/muted text. @default ['#666666', '#a1a1a1'] */
|
|
126
|
+
muted?: LightDark | undefined
|
|
127
|
+
/** Error/danger color. @default ['#e5484d', '#e5484d'] */
|
|
128
|
+
negative?: LightDark | undefined
|
|
129
|
+
/** Success color. @default ['#30a46c', '#30a46c'] */
|
|
130
|
+
positive?: LightDark | undefined
|
|
131
|
+
/** Input/card surface. @default ['#f5f5f5', '#1a1a1a'] */
|
|
132
|
+
surface?: LightDark | undefined
|
|
96
133
|
}
|
|
97
134
|
|
|
98
|
-
export
|
|
135
|
+
export type Config = {
|
|
136
|
+
text?: Text | undefined
|
|
137
|
+
theme?: Theme | undefined
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type LightDark = string | readonly [light: string, dark: string]
|
|
141
|
+
|
|
142
|
+
function resolveColor(
|
|
143
|
+
value: Theme[(typeof colorTokens)[number]] | undefined,
|
|
144
|
+
fallback: readonly [string, string],
|
|
145
|
+
): readonly [light: string, dark: string] {
|
|
146
|
+
if (!value) return fallback
|
|
147
|
+
if (typeof value === 'string') return [value, value]
|
|
148
|
+
return value
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const colorTokens = [
|
|
152
|
+
'accent',
|
|
153
|
+
'negative',
|
|
154
|
+
'positive',
|
|
155
|
+
'background',
|
|
156
|
+
'foreground',
|
|
157
|
+
'muted',
|
|
158
|
+
'surface',
|
|
159
|
+
'border',
|
|
160
|
+
] as const satisfies readonly (keyof typeof defaultTheme)[]
|
|
161
|
+
|
|
162
|
+
export function resolveOptions(options: Options): {
|
|
163
|
+
theme: ResolvedTheme
|
|
164
|
+
text: ResolvedText
|
|
165
|
+
} {
|
|
166
|
+
const theme = mergeDefined(
|
|
167
|
+
{
|
|
168
|
+
favicon: undefined as Theme['favicon'],
|
|
169
|
+
fontUrl: undefined as Theme['fontUrl'],
|
|
170
|
+
logo: undefined as Theme['logo'],
|
|
171
|
+
...defaultTheme,
|
|
172
|
+
},
|
|
173
|
+
(options.theme as never) ?? {},
|
|
174
|
+
)
|
|
175
|
+
const text = sanitizeRecord(mergeDefined(defaultText, (options.text as never) ?? {}))
|
|
176
|
+
return { theme, text }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type ResolvedTheme = {
|
|
180
|
+
[k in keyof Omit<Theme, 'favicon' | 'fontUrl' | 'logo'>]-?: NonNullable<Theme[k]>
|
|
181
|
+
} & Pick<Theme, 'favicon' | 'fontUrl' | 'logo'>
|
|
182
|
+
type ResolvedText = { [k in keyof Text]-?: NonNullable<Text[k]> }
|
|
183
|
+
|
|
184
|
+
const html = String.raw
|
|
185
|
+
|
|
186
|
+
export function render(options: {
|
|
187
|
+
entries: readonly {
|
|
188
|
+
challenge: Challenge.Challenge
|
|
189
|
+
content: string
|
|
190
|
+
}[]
|
|
191
|
+
dataMap: Record<string, Data>
|
|
192
|
+
formattedAmount: string
|
|
193
|
+
/** Whether to render panel wrappers around each entry. @default entries.length > 1 */
|
|
194
|
+
panels?: boolean | undefined
|
|
195
|
+
text: ResolvedText
|
|
196
|
+
theme: ResolvedTheme
|
|
197
|
+
}): string {
|
|
198
|
+
const { entries, dataMap, formattedAmount, text, theme } = options
|
|
199
|
+
const firstChallenge = entries[0]!.challenge
|
|
200
|
+
const hasTabs = entries.length > 1
|
|
201
|
+
const hasPanels = options.panels ?? hasTabs
|
|
202
|
+
const dataValues = Object.values(dataMap)
|
|
203
|
+
|
|
204
|
+
const tabListHtml = hasTabs
|
|
205
|
+
? html`<nav class="${classNames.tabList}" role="tablist" aria-label="Payment methods">
|
|
206
|
+
${dataValues
|
|
207
|
+
.map(
|
|
208
|
+
(data, i) =>
|
|
209
|
+
html`<button
|
|
210
|
+
class="${classNames.tab}"
|
|
211
|
+
role="tab"
|
|
212
|
+
id="mppx-tab-${i}"
|
|
213
|
+
aria-selected="${i === 0 ? 'true' : 'false'}"
|
|
214
|
+
aria-controls="mppx-panel-${i}"
|
|
215
|
+
${i !== 0 ? 'tabindex="-1"' : ''}
|
|
216
|
+
data-amount="${sanitize(data.formattedAmount)}"
|
|
217
|
+
${data.challenge.description
|
|
218
|
+
? `data-description="${sanitize(data.challenge.description)}"`
|
|
219
|
+
: ''}
|
|
220
|
+
${data.challenge.expires
|
|
221
|
+
? `data-expires="${sanitize(data.challenge.expires)}"`
|
|
222
|
+
: ''}
|
|
223
|
+
${data.challenge.expires ? `data-expires-label="${sanitize(text.expires)}"` : ''}
|
|
224
|
+
>
|
|
225
|
+
${sanitize(data.label)}
|
|
226
|
+
</button>`,
|
|
227
|
+
)
|
|
228
|
+
.join('')}
|
|
229
|
+
</nav>`
|
|
230
|
+
: ''
|
|
231
|
+
|
|
232
|
+
const panelsHtml = hasPanels
|
|
233
|
+
? entries
|
|
234
|
+
.map(
|
|
235
|
+
(_entry, i) =>
|
|
236
|
+
html`<div
|
|
237
|
+
${hasTabs ? `role="tabpanel" aria-labelledby="mppx-tab-${i}"` : ''}
|
|
238
|
+
id="mppx-panel-${i}"
|
|
239
|
+
${i !== 0 ? 'hidden' : ''}
|
|
240
|
+
>
|
|
241
|
+
<div id="${ids.root}-${i}" aria-label="Payment form"></div>
|
|
242
|
+
</div>`,
|
|
243
|
+
)
|
|
244
|
+
.join('')
|
|
245
|
+
: html`<div id="${ids.root}" aria-label="Payment form"></div>`
|
|
246
|
+
|
|
247
|
+
const contentScripts = hasTabs
|
|
248
|
+
? entries
|
|
249
|
+
.map((entry) =>
|
|
250
|
+
entry.content.replace(
|
|
251
|
+
'<script>',
|
|
252
|
+
`<script ${attrs.challengeId}="${sanitize(entry.challenge.id)}">`,
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
.join('\n')
|
|
256
|
+
: entries[0]!.content
|
|
257
|
+
|
|
258
|
+
return html`<!doctype html>
|
|
259
|
+
<html lang="en">
|
|
260
|
+
<head>
|
|
261
|
+
<meta charset="UTF-8" />
|
|
262
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
263
|
+
<meta name="robots" content="noindex" />
|
|
264
|
+
<meta name="color-scheme" content="${theme.colorScheme}" />
|
|
265
|
+
<title>${text.title}</title>
|
|
266
|
+
${preflight} ${favicon(theme, firstChallenge.realm)} ${font(theme)} ${style(theme)}
|
|
267
|
+
${hasTabs ? tabStyle() : ''}
|
|
268
|
+
</head>
|
|
269
|
+
<body>
|
|
270
|
+
<main>
|
|
271
|
+
<header class="${classNames.header}">
|
|
272
|
+
${logo(theme)}
|
|
273
|
+
<span>${text.paymentRequired}</span>
|
|
274
|
+
</header>
|
|
275
|
+
<section class="${classNames.summary}" aria-label="Payment summary">
|
|
276
|
+
<h1 class="${classNames.summaryAmount}">${sanitize(formattedAmount)}</h1>
|
|
277
|
+
${firstChallenge.description
|
|
278
|
+
? `<p class="${classNames.summaryDescription}">${sanitize(firstChallenge.description)}</p>`
|
|
279
|
+
: ''}
|
|
280
|
+
${firstChallenge.expires
|
|
281
|
+
? `<p class="${classNames.summaryExpires}">${text.expires} <time datetime="${new Date(firstChallenge.expires).toISOString()}">${new Date(firstChallenge.expires).toLocaleString()}</time></p>`
|
|
282
|
+
: ''}
|
|
283
|
+
</section>
|
|
284
|
+
${tabListHtml} ${panelsHtml}
|
|
285
|
+
<script
|
|
286
|
+
id="${ids.data}"
|
|
287
|
+
type="application/json"
|
|
288
|
+
${entries.length > 1 ? ` ${attrs.remaining}="${entries.length}"` : ''}
|
|
289
|
+
>
|
|
290
|
+
${Json.stringify(dataMap satisfies Record<string, Data>).replace(/</g, '\\u003c')}
|
|
291
|
+
</script>
|
|
292
|
+
${contentScripts} ${hasTabs ? tabScript : ''}
|
|
293
|
+
</main>
|
|
294
|
+
</body>
|
|
295
|
+
</html>`
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function style(theme: {
|
|
99
299
|
[k in keyof Omit<Theme, 'favicon' | 'fontUrl' | 'logo'>]-?: NonNullable<Theme[k]>
|
|
100
300
|
}) {
|
|
101
301
|
const colors = Object.fromEntries(
|
|
@@ -116,7 +316,6 @@ export function style(theme: {
|
|
|
116
316
|
: ''
|
|
117
317
|
return html`
|
|
118
318
|
<style>
|
|
119
|
-
${reset}
|
|
120
319
|
:root {
|
|
121
320
|
color-scheme: ${theme.colorScheme};
|
|
122
321
|
${vars.fontFamily.name}: ${theme.fontFamily};
|
|
@@ -167,12 +366,12 @@ export function style(theme: {
|
|
|
167
366
|
.${classNames.logo} {
|
|
168
367
|
max-height: 1.75rem;
|
|
169
368
|
}
|
|
170
|
-
.${
|
|
369
|
+
.${logoColorScheme('dark')} {
|
|
171
370
|
@media (prefers-color-scheme: light) {
|
|
172
371
|
display: none;
|
|
173
372
|
}
|
|
174
373
|
}
|
|
175
|
-
.${
|
|
374
|
+
.${logoColorScheme('light')} {
|
|
176
375
|
@media (prefers-color-scheme: dark) {
|
|
177
376
|
display: none;
|
|
178
377
|
}
|
|
@@ -208,21 +407,7 @@ export function style(theme: {
|
|
|
208
407
|
`
|
|
209
408
|
}
|
|
210
409
|
|
|
211
|
-
|
|
212
|
-
const existing = document.getElementById(errorId)
|
|
213
|
-
if (existing) {
|
|
214
|
-
existing.textContent = message
|
|
215
|
-
return
|
|
216
|
-
}
|
|
217
|
-
const el = document.createElement('p')
|
|
218
|
-
el.id = errorId
|
|
219
|
-
el.className = classNames.error
|
|
220
|
-
el.role = 'alert'
|
|
221
|
-
el.textContent = message
|
|
222
|
-
document.getElementById(rootId)?.after(el)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export function favicon(theme: Theme, realm: string) {
|
|
410
|
+
function favicon(theme: Theme, realm: string) {
|
|
226
411
|
if (typeof theme.favicon === 'string')
|
|
227
412
|
return html`<link rel="icon" href="${sanitize(theme.favicon)}" />`
|
|
228
413
|
if (typeof theme.favicon === 'object') {
|
|
@@ -249,7 +434,17 @@ export function favicon(theme: Theme, realm: string) {
|
|
|
249
434
|
}
|
|
250
435
|
}
|
|
251
436
|
|
|
252
|
-
|
|
437
|
+
function font(theme: Theme) {
|
|
438
|
+
if (!theme.fontUrl) return ''
|
|
439
|
+
return html`<link
|
|
440
|
+
rel="preconnect"
|
|
441
|
+
href="${sanitize(new URL(theme.fontUrl).origin)}"
|
|
442
|
+
crossorigin
|
|
443
|
+
/>
|
|
444
|
+
<link rel="stylesheet" href="${sanitize(theme.fontUrl)}" />`
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function logo(value: Theme) {
|
|
253
448
|
if (typeof value.logo === 'undefined') return ''
|
|
254
449
|
if (typeof value.logo === 'string')
|
|
255
450
|
return html`<img alt="" class="${classNames.logo}" src="${sanitize(value.logo)}" />`
|
|
@@ -258,114 +453,193 @@ export function logo(value: Theme) {
|
|
|
258
453
|
(entry) =>
|
|
259
454
|
html`<img
|
|
260
455
|
alt=""
|
|
261
|
-
class="${classNames.logo} ${
|
|
456
|
+
class="${classNames.logo} ${logoColorScheme(entry[0])}"
|
|
262
457
|
src="${sanitize(entry[1])}"
|
|
263
458
|
/>`,
|
|
264
459
|
)
|
|
265
460
|
.join('\n')
|
|
266
461
|
}
|
|
267
462
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
463
|
+
function tabStyle() {
|
|
464
|
+
return html`
|
|
465
|
+
<style>
|
|
466
|
+
.${classNames.tabList} {
|
|
467
|
+
display: flex;
|
|
468
|
+
gap: calc(${vars.spacingUnit} * 2);
|
|
469
|
+
border-bottom: 1px solid ${vars.border};
|
|
470
|
+
}
|
|
471
|
+
.${classNames.tab} {
|
|
472
|
+
background: none !important;
|
|
473
|
+
border: none !important;
|
|
474
|
+
border-bottom: 1px solid transparent !important;
|
|
475
|
+
border-radius: 0 !important;
|
|
476
|
+
color: ${vars.muted} !important;
|
|
477
|
+
cursor: pointer;
|
|
478
|
+
font-size: 0.875rem;
|
|
479
|
+
font-weight: 500;
|
|
480
|
+
margin-bottom: -1px !important;
|
|
481
|
+
padding: calc(${vars.spacingUnit} * 2) calc(${vars.spacingUnit} * 4) !important;
|
|
482
|
+
text-transform: capitalize;
|
|
483
|
+
width: auto !important;
|
|
484
|
+
}
|
|
485
|
+
.${classNames.tab}[aria-selected='true'] {
|
|
486
|
+
border-bottom-color: ${vars.foreground} !important;
|
|
487
|
+
color: ${vars.foreground} !important;
|
|
488
|
+
}
|
|
489
|
+
.${classNames.tab}:hover:not([aria-selected='true']) {
|
|
490
|
+
color: ${vars.foreground} !important;
|
|
491
|
+
}
|
|
492
|
+
.${classNames.tabPanel}[hidden] {
|
|
493
|
+
display: none;
|
|
494
|
+
}
|
|
495
|
+
</style>
|
|
496
|
+
`
|
|
277
497
|
}
|
|
278
498
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
499
|
+
// Slimmed down Tailwind preflight
|
|
500
|
+
// https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/preflight.css
|
|
501
|
+
const preflight = html`<style>
|
|
502
|
+
*,
|
|
503
|
+
::after,
|
|
504
|
+
::before,
|
|
505
|
+
::backdrop,
|
|
506
|
+
::file-selector-button {
|
|
507
|
+
box-sizing: border-box;
|
|
508
|
+
margin: 0;
|
|
509
|
+
padding: 0;
|
|
510
|
+
border: 0 solid;
|
|
511
|
+
border-color: ${vars.border};
|
|
512
|
+
}
|
|
513
|
+
html,
|
|
514
|
+
:host {
|
|
515
|
+
line-height: 1.5;
|
|
516
|
+
-webkit-text-size-adjust: 100%;
|
|
517
|
+
tab-size: 4;
|
|
518
|
+
-webkit-tap-highlight-color: transparent;
|
|
519
|
+
}
|
|
520
|
+
h1,
|
|
521
|
+
h2,
|
|
522
|
+
h3,
|
|
523
|
+
h4,
|
|
524
|
+
h5,
|
|
525
|
+
h6 {
|
|
526
|
+
font-size: inherit;
|
|
527
|
+
font-weight: inherit;
|
|
528
|
+
}
|
|
529
|
+
a {
|
|
530
|
+
color: inherit;
|
|
531
|
+
-webkit-text-decoration: inherit;
|
|
532
|
+
text-decoration: inherit;
|
|
533
|
+
}
|
|
534
|
+
b,
|
|
535
|
+
strong {
|
|
536
|
+
font-weight: bolder;
|
|
537
|
+
}
|
|
538
|
+
code,
|
|
539
|
+
kbd,
|
|
540
|
+
samp,
|
|
541
|
+
pre {
|
|
542
|
+
font-family:
|
|
543
|
+
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
544
|
+
monospace;
|
|
545
|
+
font-size: 1em;
|
|
546
|
+
}
|
|
547
|
+
small {
|
|
548
|
+
font-size: 80%;
|
|
549
|
+
}
|
|
550
|
+
ol,
|
|
551
|
+
ul,
|
|
552
|
+
menu {
|
|
553
|
+
list-style: none;
|
|
554
|
+
}
|
|
555
|
+
img,
|
|
556
|
+
svg,
|
|
557
|
+
video,
|
|
558
|
+
canvas,
|
|
559
|
+
audio,
|
|
560
|
+
iframe,
|
|
561
|
+
embed,
|
|
562
|
+
object {
|
|
563
|
+
display: block;
|
|
564
|
+
vertical-align: middle;
|
|
565
|
+
}
|
|
566
|
+
img,
|
|
567
|
+
video {
|
|
568
|
+
max-width: 100%;
|
|
569
|
+
height: auto;
|
|
570
|
+
}
|
|
571
|
+
button,
|
|
572
|
+
input,
|
|
573
|
+
select,
|
|
574
|
+
optgroup,
|
|
575
|
+
textarea,
|
|
576
|
+
::file-selector-button {
|
|
577
|
+
font: inherit;
|
|
578
|
+
font-feature-settings: inherit;
|
|
579
|
+
font-variation-settings: inherit;
|
|
580
|
+
letter-spacing: inherit;
|
|
581
|
+
color: inherit;
|
|
582
|
+
border-radius: 0;
|
|
583
|
+
background-color: transparent;
|
|
584
|
+
opacity: 1;
|
|
585
|
+
}
|
|
586
|
+
::file-selector-button {
|
|
587
|
+
margin-inline-end: 4px;
|
|
588
|
+
}
|
|
589
|
+
::placeholder {
|
|
590
|
+
opacity: 1;
|
|
591
|
+
}
|
|
592
|
+
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
|
593
|
+
::placeholder {
|
|
594
|
+
color: color-mix(in oklab, currentcolor 50%, transparent);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
textarea {
|
|
598
|
+
resize: vertical;
|
|
599
|
+
}
|
|
600
|
+
::-webkit-search-decoration {
|
|
601
|
+
-webkit-appearance: none;
|
|
602
|
+
}
|
|
603
|
+
:-moz-ui-invalid {
|
|
604
|
+
box-shadow: none;
|
|
605
|
+
}
|
|
606
|
+
button,
|
|
607
|
+
input:where([type='button'], [type='reset'], [type='submit']),
|
|
608
|
+
::file-selector-button {
|
|
609
|
+
appearance: button;
|
|
610
|
+
}
|
|
611
|
+
::-webkit-inner-spin-button,
|
|
612
|
+
::-webkit-outer-spin-button {
|
|
613
|
+
height: auto;
|
|
614
|
+
}
|
|
615
|
+
[hidden]:where(:not([hidden='until-found'])) {
|
|
616
|
+
display: none !important;
|
|
617
|
+
}
|
|
618
|
+
</style>`
|
|
303
619
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
foreground?: LightDark | undefined
|
|
312
|
-
/** Secondary/muted text. @default ['#666666', '#a1a1a1'] */
|
|
313
|
-
muted?: LightDark | undefined
|
|
314
|
-
/** Error/danger color. @default ['#e5484d', '#e5484d'] */
|
|
315
|
-
negative?: LightDark | undefined
|
|
316
|
-
/** Success color. @default ['#30a46c', '#30a46c'] */
|
|
317
|
-
positive?: LightDark | undefined
|
|
318
|
-
/** Input/card surface. @default ['#f5f5f5', '#1a1a1a'] */
|
|
319
|
-
surface?: LightDark | undefined
|
|
620
|
+
function sanitize(str: string): string {
|
|
621
|
+
return str
|
|
622
|
+
.replace(/&/g, '&')
|
|
623
|
+
.replace(/</g, '<')
|
|
624
|
+
.replace(/>/g, '>')
|
|
625
|
+
.replace(/"/g, '"')
|
|
626
|
+
.replace(/'/g, ''')
|
|
320
627
|
}
|
|
321
628
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
327
|
-
fontSizeBase: '16px',
|
|
328
|
-
radius: '6px',
|
|
329
|
-
spacingUnit: '2px',
|
|
330
|
-
|
|
331
|
-
accent: ['#171717', '#ededed'],
|
|
332
|
-
background: ['#ffffff', '#0a0a0a'],
|
|
333
|
-
border: ['#e5e5e5', '#2e2e2e'],
|
|
334
|
-
foreground: ['#0a0a0a', '#ededed'],
|
|
335
|
-
muted: ['#666666', '#a1a1a1'],
|
|
336
|
-
negative: ['#e5484d', '#e5484d'],
|
|
337
|
-
positive: ['#30a46c', '#30a46c'],
|
|
338
|
-
surface: ['#f5f5f5', '#1a1a1a'],
|
|
339
|
-
} as const satisfies Required<Omit<Theme, 'favicon' | 'fontUrl' | 'logo'>>
|
|
340
|
-
|
|
341
|
-
export const colorTokens = [
|
|
342
|
-
'accent',
|
|
343
|
-
'negative',
|
|
344
|
-
'positive',
|
|
345
|
-
'background',
|
|
346
|
-
'foreground',
|
|
347
|
-
'muted',
|
|
348
|
-
'surface',
|
|
349
|
-
'border',
|
|
350
|
-
] as const satisfies readonly (keyof typeof defaultTheme)[]
|
|
351
|
-
|
|
352
|
-
export function resolveColor(
|
|
353
|
-
value: Theme[(typeof colorTokens)[number]] | undefined,
|
|
354
|
-
fallback: readonly [string, string],
|
|
355
|
-
): readonly [light: string, dark: string] {
|
|
356
|
-
if (!value) return fallback
|
|
357
|
-
if (typeof value === 'string') return [value, value]
|
|
358
|
-
return value
|
|
629
|
+
function sanitizeRecord<type extends Record<string, string>>(record: type): type {
|
|
630
|
+
return Object.fromEntries(
|
|
631
|
+
Object.entries(record).map(([key, value]) => [key, sanitize(value)]),
|
|
632
|
+
) as type
|
|
359
633
|
}
|
|
360
634
|
|
|
361
635
|
export function mergeDefined<type>(defaults: type, value: DeepPartial<type> | undefined): type {
|
|
362
|
-
if (value === undefined) return defaults
|
|
636
|
+
if (value === undefined || value === null) return defaults
|
|
363
637
|
if (!isPlainObject(defaults) || !isPlainObject(value)) return (value ?? defaults) as type
|
|
364
638
|
|
|
365
639
|
const result: Record<string, unknown> = { ...defaults }
|
|
366
640
|
|
|
367
641
|
for (const [key, nextValue] of Object.entries(value)) {
|
|
368
|
-
if (nextValue === undefined) continue
|
|
642
|
+
if (nextValue === undefined || nextValue === null || nextValue === '') continue
|
|
369
643
|
|
|
370
644
|
const currentValue = result[key]
|
|
371
645
|
|
|
@@ -388,27 +662,3 @@ type DeepPartial<type> = {
|
|
|
388
662
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
389
663
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
390
664
|
}
|
|
391
|
-
|
|
392
|
-
// Slimmed down Tailwind preflight
|
|
393
|
-
// https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/preflight.css
|
|
394
|
-
const reset = html`
|
|
395
|
-
*, ::after, ::before, ::backdrop, ::file-selector-button { box-sizing: border-box; margin: 0;
|
|
396
|
-
padding: 0; border: 0 solid; border-color: ${vars.border}; } html, :host { line-height: 1.5;
|
|
397
|
-
-webkit-text-size-adjust: 100%; tab-size: 4; -webkit-tap-highlight-color: transparent; } h1, h2,
|
|
398
|
-
h3, h4, h5, h6 { font-size: inherit; font-weight: inherit; } a { color: inherit;
|
|
399
|
-
-webkit-text-decoration: inherit; text-decoration: inherit; } b, strong { font-weight: bolder; }
|
|
400
|
-
code, kbd, samp, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
|
401
|
-
'Liberation Mono', 'Courier New', monospace; font-size: 1em; } small { font-size: 80%; } ol, ul,
|
|
402
|
-
menu { list-style: none; } img, svg, video, canvas, audio, iframe, embed, object { display: block;
|
|
403
|
-
vertical-align: middle; } img, video { max-width: 100%; height: auto; } button, input, select,
|
|
404
|
-
optgroup, textarea, ::file-selector-button { font: inherit; font-feature-settings: inherit;
|
|
405
|
-
font-variation-settings: inherit; letter-spacing: inherit; color: inherit; border-radius: 0;
|
|
406
|
-
background-color: transparent; opacity: 1; } ::file-selector-button { margin-inline-end: 4px; }
|
|
407
|
-
::placeholder { opacity: 1; } @supports (not (-webkit-appearance: -apple-pay-button)) or
|
|
408
|
-
(contain-intrinsic-size: 1px) { ::placeholder { color: color-mix(in oklab, currentcolor 50%,
|
|
409
|
-
transparent); } } textarea { resize: vertical; } ::-webkit-search-decoration { -webkit-appearance:
|
|
410
|
-
none; } :-moz-ui-invalid { box-shadow: none; } button, input:where([type='button'],
|
|
411
|
-
[type='reset'], [type='submit']), ::file-selector-button { appearance: button; }
|
|
412
|
-
::-webkit-inner-spin-button, ::-webkit-outer-spin-button { height: auto; }
|
|
413
|
-
[hidden]:where(:not([hidden='until-found'])) { display: none !important; }
|
|
414
|
-
`
|