mppx 0.5.4 → 0.5.6
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 +14 -0
- package/dist/Html.d.ts +10 -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 +46 -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/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/Attribution.d.ts +24 -7
- package/dist/tempo/Attribution.d.ts.map +1 -1
- package/dist/tempo/Attribution.js +33 -7
- package/dist/tempo/Attribution.js.map +1 -1
- package/dist/tempo/client/Charge.js +1 -1
- package/dist/tempo/client/Charge.js.map +1 -1
- package/dist/tempo/server/Charge.d.ts.map +1 -1
- package/dist/tempo/server/Charge.js +36 -2
- 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 +57 -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 +422 -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/internal/html/main.ts +44 -53
- package/src/stripe/server/internal/html.gen.ts +1 -1
- package/src/tempo/Attribution.test.ts +129 -23
- package/src/tempo/Attribution.ts +39 -10
- package/src/tempo/client/Charge.ts +1 -1
- package/src/tempo/server/Charge.test.ts +205 -5
- package/src/tempo/server/Charge.ts +54 -3
- 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,236 @@ 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
|
|
96
80
|
}
|
|
97
81
|
|
|
98
|
-
export
|
|
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
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
type LightDark = string | readonly [light: string, dark: string]
|
|
136
|
+
|
|
137
|
+
function resolveColor(
|
|
138
|
+
value: Theme[(typeof colorTokens)[number]] | undefined,
|
|
139
|
+
fallback: readonly [string, string],
|
|
140
|
+
): readonly [light: string, dark: string] {
|
|
141
|
+
if (!value) return fallback
|
|
142
|
+
if (typeof value === 'string') return [value, value]
|
|
143
|
+
return value
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const colorTokens = [
|
|
147
|
+
'accent',
|
|
148
|
+
'negative',
|
|
149
|
+
'positive',
|
|
150
|
+
'background',
|
|
151
|
+
'foreground',
|
|
152
|
+
'muted',
|
|
153
|
+
'surface',
|
|
154
|
+
'border',
|
|
155
|
+
] as const satisfies readonly (keyof typeof defaultTheme)[]
|
|
156
|
+
|
|
157
|
+
export function resolveOptions(options: Options): {
|
|
158
|
+
theme: ResolvedTheme
|
|
159
|
+
text: ResolvedText
|
|
160
|
+
} {
|
|
161
|
+
const theme = mergeDefined(
|
|
162
|
+
{
|
|
163
|
+
favicon: undefined as Theme['favicon'],
|
|
164
|
+
fontUrl: undefined as Theme['fontUrl'],
|
|
165
|
+
logo: undefined as Theme['logo'],
|
|
166
|
+
...defaultTheme,
|
|
167
|
+
},
|
|
168
|
+
(options.theme as never) ?? {},
|
|
169
|
+
)
|
|
170
|
+
const text = sanitizeRecord(mergeDefined(defaultText, (options.text as never) ?? {}))
|
|
171
|
+
return { theme, text }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
type ResolvedTheme = {
|
|
175
|
+
[k in keyof Omit<Theme, 'favicon' | 'fontUrl' | 'logo'>]-?: NonNullable<Theme[k]>
|
|
176
|
+
} & Pick<Theme, 'favicon' | 'fontUrl' | 'logo'>
|
|
177
|
+
type ResolvedText = { [k in keyof Text]-?: NonNullable<Text[k]> }
|
|
178
|
+
|
|
179
|
+
const html = String.raw
|
|
180
|
+
|
|
181
|
+
export function render(options: {
|
|
182
|
+
entries: readonly {
|
|
183
|
+
challenge: Challenge.Challenge
|
|
184
|
+
content: string
|
|
185
|
+
}[]
|
|
186
|
+
dataMap: Record<string, Data>
|
|
187
|
+
formattedAmount: string
|
|
188
|
+
/** Whether to render panel wrappers around each entry. @default entries.length > 1 */
|
|
189
|
+
panels?: boolean | undefined
|
|
190
|
+
text: ResolvedText
|
|
191
|
+
theme: ResolvedTheme
|
|
192
|
+
}): string {
|
|
193
|
+
const { entries, dataMap, formattedAmount, text, theme } = options
|
|
194
|
+
const firstChallenge = entries[0]!.challenge
|
|
195
|
+
const hasTabs = entries.length > 1
|
|
196
|
+
const hasPanels = options.panels ?? hasTabs
|
|
197
|
+
const dataValues = Object.values(dataMap)
|
|
198
|
+
|
|
199
|
+
const tabListHtml = hasTabs
|
|
200
|
+
? html`<nav class="${classNames.tabList}" role="tablist" aria-label="Payment methods">
|
|
201
|
+
${dataValues
|
|
202
|
+
.map(
|
|
203
|
+
(data, i) =>
|
|
204
|
+
html`<button
|
|
205
|
+
class="${classNames.tab}"
|
|
206
|
+
role="tab"
|
|
207
|
+
id="mppx-tab-${i}"
|
|
208
|
+
aria-selected="${i === 0 ? 'true' : 'false'}"
|
|
209
|
+
aria-controls="mppx-panel-${i}"
|
|
210
|
+
${i !== 0 ? 'tabindex="-1"' : ''}
|
|
211
|
+
data-amount="${sanitize(data.formattedAmount)}"
|
|
212
|
+
${data.challenge.description
|
|
213
|
+
? `data-description="${sanitize(data.challenge.description)}"`
|
|
214
|
+
: ''}
|
|
215
|
+
${data.challenge.expires
|
|
216
|
+
? `data-expires="${sanitize(data.challenge.expires)}"`
|
|
217
|
+
: ''}
|
|
218
|
+
${data.challenge.expires ? `data-expires-label="${sanitize(text.expires)}"` : ''}
|
|
219
|
+
>
|
|
220
|
+
${sanitize(data.label)}
|
|
221
|
+
</button>`,
|
|
222
|
+
)
|
|
223
|
+
.join('')}
|
|
224
|
+
</nav>`
|
|
225
|
+
: ''
|
|
226
|
+
|
|
227
|
+
const panelsHtml = hasPanels
|
|
228
|
+
? entries
|
|
229
|
+
.map(
|
|
230
|
+
(_entry, i) =>
|
|
231
|
+
html`<div
|
|
232
|
+
${hasTabs ? `role="tabpanel" aria-labelledby="mppx-tab-${i}"` : ''}
|
|
233
|
+
id="mppx-panel-${i}"
|
|
234
|
+
${i !== 0 ? 'hidden' : ''}
|
|
235
|
+
>
|
|
236
|
+
<div id="${ids.root}-${i}" aria-label="Payment form"></div>
|
|
237
|
+
</div>`,
|
|
238
|
+
)
|
|
239
|
+
.join('')
|
|
240
|
+
: html`<div id="${ids.root}" aria-label="Payment form"></div>`
|
|
241
|
+
|
|
242
|
+
const contentScripts = hasTabs
|
|
243
|
+
? entries
|
|
244
|
+
.map((entry) =>
|
|
245
|
+
entry.content.replace(
|
|
246
|
+
'<script>',
|
|
247
|
+
`<script ${attrs.challengeId}="${sanitize(entry.challenge.id)}">`,
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
.join('\n')
|
|
251
|
+
: entries[0]!.content
|
|
252
|
+
|
|
253
|
+
return html`<!doctype html>
|
|
254
|
+
<html lang="en">
|
|
255
|
+
<head>
|
|
256
|
+
<meta charset="UTF-8" />
|
|
257
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
258
|
+
<meta name="robots" content="noindex" />
|
|
259
|
+
<meta name="color-scheme" content="${theme.colorScheme}" />
|
|
260
|
+
<title>${text.title}</title>
|
|
261
|
+
${preflight} ${favicon(theme, firstChallenge.realm)} ${font(theme)} ${style(theme)}
|
|
262
|
+
${hasTabs ? tabStyle() : ''}
|
|
263
|
+
</head>
|
|
264
|
+
<body>
|
|
265
|
+
<main>
|
|
266
|
+
<header class="${classNames.header}">
|
|
267
|
+
${logo(theme)}
|
|
268
|
+
<span>${text.paymentRequired}</span>
|
|
269
|
+
</header>
|
|
270
|
+
<section class="${classNames.summary}" aria-label="Payment summary">
|
|
271
|
+
<h1 class="${classNames.summaryAmount}">${sanitize(formattedAmount)}</h1>
|
|
272
|
+
${firstChallenge.description
|
|
273
|
+
? `<p class="${classNames.summaryDescription}">${sanitize(firstChallenge.description)}</p>`
|
|
274
|
+
: ''}
|
|
275
|
+
${firstChallenge.expires
|
|
276
|
+
? `<p class="${classNames.summaryExpires}">${text.expires} <time datetime="${new Date(firstChallenge.expires).toISOString()}">${new Date(firstChallenge.expires).toLocaleString()}</time></p>`
|
|
277
|
+
: ''}
|
|
278
|
+
</section>
|
|
279
|
+
${tabListHtml} ${panelsHtml}
|
|
280
|
+
<script
|
|
281
|
+
id="${ids.data}"
|
|
282
|
+
type="application/json"
|
|
283
|
+
${entries.length > 1 ? ` ${attrs.remaining}="${entries.length}"` : ''}
|
|
284
|
+
>
|
|
285
|
+
${Json.stringify(dataMap satisfies Record<string, Data>).replace(/</g, '\\u003c')}
|
|
286
|
+
</script>
|
|
287
|
+
${contentScripts} ${hasTabs ? tabScript : ''}
|
|
288
|
+
</main>
|
|
289
|
+
</body>
|
|
290
|
+
</html>`
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function style(theme: {
|
|
99
294
|
[k in keyof Omit<Theme, 'favicon' | 'fontUrl' | 'logo'>]-?: NonNullable<Theme[k]>
|
|
100
295
|
}) {
|
|
101
296
|
const colors = Object.fromEntries(
|
|
@@ -116,7 +311,6 @@ export function style(theme: {
|
|
|
116
311
|
: ''
|
|
117
312
|
return html`
|
|
118
313
|
<style>
|
|
119
|
-
${reset}
|
|
120
314
|
:root {
|
|
121
315
|
color-scheme: ${theme.colorScheme};
|
|
122
316
|
${vars.fontFamily.name}: ${theme.fontFamily};
|
|
@@ -167,12 +361,12 @@ export function style(theme: {
|
|
|
167
361
|
.${classNames.logo} {
|
|
168
362
|
max-height: 1.75rem;
|
|
169
363
|
}
|
|
170
|
-
.${
|
|
364
|
+
.${logoColorScheme('dark')} {
|
|
171
365
|
@media (prefers-color-scheme: light) {
|
|
172
366
|
display: none;
|
|
173
367
|
}
|
|
174
368
|
}
|
|
175
|
-
.${
|
|
369
|
+
.${logoColorScheme('light')} {
|
|
176
370
|
@media (prefers-color-scheme: dark) {
|
|
177
371
|
display: none;
|
|
178
372
|
}
|
|
@@ -208,21 +402,7 @@ export function style(theme: {
|
|
|
208
402
|
`
|
|
209
403
|
}
|
|
210
404
|
|
|
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) {
|
|
405
|
+
function favicon(theme: Theme, realm: string) {
|
|
226
406
|
if (typeof theme.favicon === 'string')
|
|
227
407
|
return html`<link rel="icon" href="${sanitize(theme.favicon)}" />`
|
|
228
408
|
if (typeof theme.favicon === 'object') {
|
|
@@ -249,7 +429,17 @@ export function favicon(theme: Theme, realm: string) {
|
|
|
249
429
|
}
|
|
250
430
|
}
|
|
251
431
|
|
|
252
|
-
|
|
432
|
+
function font(theme: Theme) {
|
|
433
|
+
if (!theme.fontUrl) return ''
|
|
434
|
+
return html`<link
|
|
435
|
+
rel="preconnect"
|
|
436
|
+
href="${sanitize(new URL(theme.fontUrl).origin)}"
|
|
437
|
+
crossorigin
|
|
438
|
+
/>
|
|
439
|
+
<link rel="stylesheet" href="${sanitize(theme.fontUrl)}" />`
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function logo(value: Theme) {
|
|
253
443
|
if (typeof value.logo === 'undefined') return ''
|
|
254
444
|
if (typeof value.logo === 'string')
|
|
255
445
|
return html`<img alt="" class="${classNames.logo}" src="${sanitize(value.logo)}" />`
|
|
@@ -258,114 +448,193 @@ export function logo(value: Theme) {
|
|
|
258
448
|
(entry) =>
|
|
259
449
|
html`<img
|
|
260
450
|
alt=""
|
|
261
|
-
class="${classNames.logo} ${
|
|
451
|
+
class="${classNames.logo} ${logoColorScheme(entry[0])}"
|
|
262
452
|
src="${sanitize(entry[1])}"
|
|
263
453
|
/>`,
|
|
264
454
|
)
|
|
265
455
|
.join('\n')
|
|
266
456
|
}
|
|
267
457
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
458
|
+
function tabStyle() {
|
|
459
|
+
return html`
|
|
460
|
+
<style>
|
|
461
|
+
.${classNames.tabList} {
|
|
462
|
+
display: flex;
|
|
463
|
+
gap: calc(${vars.spacingUnit} * 2);
|
|
464
|
+
border-bottom: 1px solid ${vars.border};
|
|
465
|
+
}
|
|
466
|
+
.${classNames.tab} {
|
|
467
|
+
background: none !important;
|
|
468
|
+
border: none !important;
|
|
469
|
+
border-bottom: 1px solid transparent !important;
|
|
470
|
+
border-radius: 0 !important;
|
|
471
|
+
color: ${vars.muted} !important;
|
|
472
|
+
cursor: pointer;
|
|
473
|
+
font-size: 0.875rem;
|
|
474
|
+
font-weight: 500;
|
|
475
|
+
margin-bottom: -1px !important;
|
|
476
|
+
padding: calc(${vars.spacingUnit} * 2) calc(${vars.spacingUnit} * 4) !important;
|
|
477
|
+
text-transform: capitalize;
|
|
478
|
+
width: auto !important;
|
|
479
|
+
}
|
|
480
|
+
.${classNames.tab}[aria-selected='true'] {
|
|
481
|
+
border-bottom-color: ${vars.foreground} !important;
|
|
482
|
+
color: ${vars.foreground} !important;
|
|
483
|
+
}
|
|
484
|
+
.${classNames.tab}:hover:not([aria-selected='true']) {
|
|
485
|
+
color: ${vars.foreground} !important;
|
|
486
|
+
}
|
|
487
|
+
.${classNames.tabPanel}[hidden] {
|
|
488
|
+
display: none;
|
|
489
|
+
}
|
|
490
|
+
</style>
|
|
491
|
+
`
|
|
277
492
|
}
|
|
278
493
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
494
|
+
// Slimmed down Tailwind preflight
|
|
495
|
+
// https://github.com/tailwindlabs/tailwindcss/blob/main/packages/tailwindcss/preflight.css
|
|
496
|
+
const preflight = html`<style>
|
|
497
|
+
*,
|
|
498
|
+
::after,
|
|
499
|
+
::before,
|
|
500
|
+
::backdrop,
|
|
501
|
+
::file-selector-button {
|
|
502
|
+
box-sizing: border-box;
|
|
503
|
+
margin: 0;
|
|
504
|
+
padding: 0;
|
|
505
|
+
border: 0 solid;
|
|
506
|
+
border-color: ${vars.border};
|
|
507
|
+
}
|
|
508
|
+
html,
|
|
509
|
+
:host {
|
|
510
|
+
line-height: 1.5;
|
|
511
|
+
-webkit-text-size-adjust: 100%;
|
|
512
|
+
tab-size: 4;
|
|
513
|
+
-webkit-tap-highlight-color: transparent;
|
|
514
|
+
}
|
|
515
|
+
h1,
|
|
516
|
+
h2,
|
|
517
|
+
h3,
|
|
518
|
+
h4,
|
|
519
|
+
h5,
|
|
520
|
+
h6 {
|
|
521
|
+
font-size: inherit;
|
|
522
|
+
font-weight: inherit;
|
|
523
|
+
}
|
|
524
|
+
a {
|
|
525
|
+
color: inherit;
|
|
526
|
+
-webkit-text-decoration: inherit;
|
|
527
|
+
text-decoration: inherit;
|
|
528
|
+
}
|
|
529
|
+
b,
|
|
530
|
+
strong {
|
|
531
|
+
font-weight: bolder;
|
|
532
|
+
}
|
|
533
|
+
code,
|
|
534
|
+
kbd,
|
|
535
|
+
samp,
|
|
536
|
+
pre {
|
|
537
|
+
font-family:
|
|
538
|
+
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
539
|
+
monospace;
|
|
540
|
+
font-size: 1em;
|
|
541
|
+
}
|
|
542
|
+
small {
|
|
543
|
+
font-size: 80%;
|
|
544
|
+
}
|
|
545
|
+
ol,
|
|
546
|
+
ul,
|
|
547
|
+
menu {
|
|
548
|
+
list-style: none;
|
|
549
|
+
}
|
|
550
|
+
img,
|
|
551
|
+
svg,
|
|
552
|
+
video,
|
|
553
|
+
canvas,
|
|
554
|
+
audio,
|
|
555
|
+
iframe,
|
|
556
|
+
embed,
|
|
557
|
+
object {
|
|
558
|
+
display: block;
|
|
559
|
+
vertical-align: middle;
|
|
560
|
+
}
|
|
561
|
+
img,
|
|
562
|
+
video {
|
|
563
|
+
max-width: 100%;
|
|
564
|
+
height: auto;
|
|
565
|
+
}
|
|
566
|
+
button,
|
|
567
|
+
input,
|
|
568
|
+
select,
|
|
569
|
+
optgroup,
|
|
570
|
+
textarea,
|
|
571
|
+
::file-selector-button {
|
|
572
|
+
font: inherit;
|
|
573
|
+
font-feature-settings: inherit;
|
|
574
|
+
font-variation-settings: inherit;
|
|
575
|
+
letter-spacing: inherit;
|
|
576
|
+
color: inherit;
|
|
577
|
+
border-radius: 0;
|
|
578
|
+
background-color: transparent;
|
|
579
|
+
opacity: 1;
|
|
580
|
+
}
|
|
581
|
+
::file-selector-button {
|
|
582
|
+
margin-inline-end: 4px;
|
|
583
|
+
}
|
|
584
|
+
::placeholder {
|
|
585
|
+
opacity: 1;
|
|
586
|
+
}
|
|
587
|
+
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
|
|
588
|
+
::placeholder {
|
|
589
|
+
color: color-mix(in oklab, currentcolor 50%, transparent);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
textarea {
|
|
593
|
+
resize: vertical;
|
|
594
|
+
}
|
|
595
|
+
::-webkit-search-decoration {
|
|
596
|
+
-webkit-appearance: none;
|
|
597
|
+
}
|
|
598
|
+
:-moz-ui-invalid {
|
|
599
|
+
box-shadow: none;
|
|
600
|
+
}
|
|
601
|
+
button,
|
|
602
|
+
input:where([type='button'], [type='reset'], [type='submit']),
|
|
603
|
+
::file-selector-button {
|
|
604
|
+
appearance: button;
|
|
605
|
+
}
|
|
606
|
+
::-webkit-inner-spin-button,
|
|
607
|
+
::-webkit-outer-spin-button {
|
|
608
|
+
height: auto;
|
|
609
|
+
}
|
|
610
|
+
[hidden]:where(:not([hidden='until-found'])) {
|
|
611
|
+
display: none !important;
|
|
612
|
+
}
|
|
613
|
+
</style>`
|
|
303
614
|
|
|
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
|
|
615
|
+
function sanitize(str: string): string {
|
|
616
|
+
return str
|
|
617
|
+
.replace(/&/g, '&')
|
|
618
|
+
.replace(/</g, '<')
|
|
619
|
+
.replace(/>/g, '>')
|
|
620
|
+
.replace(/"/g, '"')
|
|
621
|
+
.replace(/'/g, ''')
|
|
320
622
|
}
|
|
321
623
|
|
|
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
|
|
624
|
+
function sanitizeRecord<type extends Record<string, string>>(record: type): type {
|
|
625
|
+
return Object.fromEntries(
|
|
626
|
+
Object.entries(record).map(([key, value]) => [key, sanitize(value)]),
|
|
627
|
+
) as type
|
|
359
628
|
}
|
|
360
629
|
|
|
361
630
|
export function mergeDefined<type>(defaults: type, value: DeepPartial<type> | undefined): type {
|
|
362
|
-
if (value === undefined) return defaults
|
|
631
|
+
if (value === undefined || value === null) return defaults
|
|
363
632
|
if (!isPlainObject(defaults) || !isPlainObject(value)) return (value ?? defaults) as type
|
|
364
633
|
|
|
365
634
|
const result: Record<string, unknown> = { ...defaults }
|
|
366
635
|
|
|
367
636
|
for (const [key, nextValue] of Object.entries(value)) {
|
|
368
|
-
if (nextValue === undefined) continue
|
|
637
|
+
if (nextValue === undefined || nextValue === null || nextValue === '') continue
|
|
369
638
|
|
|
370
639
|
const currentValue = result[key]
|
|
371
640
|
|
|
@@ -388,27 +657,3 @@ type DeepPartial<type> = {
|
|
|
388
657
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
389
658
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
390
659
|
}
|
|
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
|
-
`
|