@purestack/ts-components 1.1.0

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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -0
  3. package/dist/ts-components.d.mts +1662 -0
  4. package/dist/ts-components.d.mts.map +1 -0
  5. package/dist/ts-components.mjs +9200 -0
  6. package/dist/ts-components.mjs.map +1 -0
  7. package/package.json +130 -0
  8. package/src/defineComponents.ts +87 -0
  9. package/src/index.ts +270 -0
  10. package/src/registerStyles.ts +63 -0
  11. package/src/standard/alertBox/alertBox.ts +64 -0
  12. package/src/standard/autoId.ts +25 -0
  13. package/src/standard/badge/badge.ts +41 -0
  14. package/src/standard/badge/badgeStyle.ts +21 -0
  15. package/src/standard/barChart/barChart.ts +338 -0
  16. package/src/standard/barChart/barChartStyle.ts +81 -0
  17. package/src/standard/btn/btn.ts +191 -0
  18. package/src/standard/btn/btnStyle.ts +59 -0
  19. package/src/standard/btnGroup/btnGroup.ts +208 -0
  20. package/src/standard/btnGroup/btnGroupStyle.ts +85 -0
  21. package/src/standard/chart/cartesianChart.ts +99 -0
  22. package/src/standard/chart/chartDefaults.ts +25 -0
  23. package/src/standard/chart/chartUtils.ts +84 -0
  24. package/src/standard/classicLogo/classicLogo.ts +429 -0
  25. package/src/standard/classicLogo/classicLogoStyle.ts +292 -0
  26. package/src/standard/componentVariant.ts +153 -0
  27. package/src/standard/composer/composer.ts +1035 -0
  28. package/src/standard/composer/composerCanvasInitialStyle.ts +225 -0
  29. package/src/standard/composer/composerCanvasRevertStyle.ts +14 -0
  30. package/src/standard/composer/composerCanvasSharedStyle.ts +60 -0
  31. package/src/standard/composer/composerCanvasStyleTypes.ts +8 -0
  32. package/src/standard/composer/composerCanvasThemeStyle.ts +53 -0
  33. package/src/standard/composer/composerStyle.ts +156 -0
  34. package/src/standard/composer/emailHtmlPolicy.ts +474 -0
  35. package/src/standard/consent/consent.ts +173 -0
  36. package/src/standard/consent/consentStyle.ts +44 -0
  37. package/src/standard/contactForm/contactForm.ts +135 -0
  38. package/src/standard/contactForm/contactFormStyle.ts +116 -0
  39. package/src/standard/doughnutChart/doughnutChart.ts +527 -0
  40. package/src/standard/doughnutChart/doughnutChartStyle.ts +64 -0
  41. package/src/standard/dropFiles/dropFiles.ts +480 -0
  42. package/src/standard/dropFiles/dropFilesStyle.ts +171 -0
  43. package/src/standard/expandablePanel/expandablePanel.ts +106 -0
  44. package/src/standard/expandablePanel/expandablePanelStyle.ts +56 -0
  45. package/src/standard/flex/flex.ts +269 -0
  46. package/src/standard/flex/flexStyle.ts +556 -0
  47. package/src/standard/footer/footer.ts +93 -0
  48. package/src/standard/footer/footerStyle.ts +39 -0
  49. package/src/standard/form/autoCompleteInput.ts +567 -0
  50. package/src/standard/form/form.ts +205 -0
  51. package/src/standard/form/formCheck.ts +96 -0
  52. package/src/standard/form/formInputField.ts +184 -0
  53. package/src/standard/form/formSelectField.ts +152 -0
  54. package/src/standard/form/formStyle.ts +585 -0
  55. package/src/standard/form/multiAutoCompleteInput.ts +880 -0
  56. package/src/standard/grid/grid.ts +177 -0
  57. package/src/standard/grid/gridStyle.ts +56 -0
  58. package/src/standard/icon/icon.ts +96 -0
  59. package/src/standard/icon/iconStyle.ts +50 -0
  60. package/src/standard/landing/codeShowcase.ts +54 -0
  61. package/src/standard/landing/comparisonColumn.ts +30 -0
  62. package/src/standard/landing/comparisonFeature.ts +22 -0
  63. package/src/standard/landing/comparisonTable.ts +38 -0
  64. package/src/standard/landing/ctaSection.ts +59 -0
  65. package/src/standard/landing/featureCard.ts +41 -0
  66. package/src/standard/landing/landing.ts +52 -0
  67. package/src/standard/landing/landingBand.ts +217 -0
  68. package/src/standard/landing/landingSection.ts +43 -0
  69. package/src/standard/landing/landingTypes.ts +141 -0
  70. package/src/standard/landing/metricItem.ts +25 -0
  71. package/src/standard/landing/metricStrip.ts +20 -0
  72. package/src/standard/lineChart/lineChart.ts +462 -0
  73. package/src/standard/lineChart/lineChartStyle.ts +91 -0
  74. package/src/standard/logo/logo.ts +192 -0
  75. package/src/standard/logo/logoStyle.ts +164 -0
  76. package/src/standard/modal/modal.ts +171 -0
  77. package/src/standard/modal/modalStore.ts +154 -0
  78. package/src/standard/modal/modalStyle.ts +190 -0
  79. package/src/standard/navMenu/navMenu.ts +278 -0
  80. package/src/standard/navMenu/navMenuStyle.ts +323 -0
  81. package/src/standard/pageLinks/pageLinks.ts +90 -0
  82. package/src/standard/pageScript/pageScript.ts +155 -0
  83. package/src/standard/pageToc/pageToc.ts +154 -0
  84. package/src/standard/pageToc/pageTocStyle.ts +372 -0
  85. package/src/standard/panel/panel.ts +55 -0
  86. package/src/standard/panel/panelStyle.ts +15 -0
  87. package/src/standard/pricing/pricing.ts +146 -0
  88. package/src/standard/searchBox/searchBox.ts +54 -0
  89. package/src/standard/searchBox/searchBoxStyle.ts +180 -0
  90. package/src/standard/sectionHeader/sectionHeader.ts +99 -0
  91. package/src/standard/signIn/signIn.ts +167 -0
  92. package/src/standard/signIn/signInStyle.ts +70 -0
  93. package/src/standard/tabs/tabs.ts +193 -0
  94. package/src/standard/tabs/tabsStyle.ts +231 -0
  95. package/src/standard/themeToggle/themeToggle.ts +33 -0
  96. package/src/standard/themeToggle/themeToggleStyle.ts +88 -0
  97. package/src/standard/toast/toastHost.ts +42 -0
  98. package/src/standard/toast/toastStore.ts +50 -0
  99. package/src/standard/topBar/topBar.ts +101 -0
  100. package/src/standard/topBar/topBarStyle.ts +169 -0
  101. package/src/standard/virtualList/variableVirtualTable.ts +371 -0
  102. package/src/standard/virtualList/virtualList.ts +423 -0
  103. package/src/standard/virtualList/virtualListStyle.ts +34 -0
  104. package/src/standard/virtualList/virtualTable.ts +179 -0
  105. package/src/standard/virtualList/virtualTableStyle.ts +24 -0
  106. package/tsconfig.json +23 -0
@@ -0,0 +1,323 @@
1
+ import type { ThemePalette } from '@purestack/ts-style'
2
+ import {
3
+ BREAKPOINTS,
4
+ docLayoutVar,
5
+ mediaAbove,
6
+ mediaBelow,
7
+ mediaMax,
8
+ mediaMin,
9
+ styleBuilder,
10
+ type ThemeMode,
11
+ themes,
12
+ } from '@purestack/ts-style'
13
+ import { getTopBarToggleVisibleSelectors } from '../topBar/topBarStyle'
14
+
15
+ export function registerNavStyles() {
16
+ themes.forEach((theme, palette) => {
17
+ registerNavShellStyles(theme, palette)
18
+ registerNavSummaryStyles(theme, palette)
19
+ })
20
+ }
21
+
22
+ export function registerNavShellStyles(
23
+ theme: ThemeMode,
24
+ palette: ThemePalette,
25
+ ) {
26
+ styleBuilder
27
+ .select('li.nav__item', theme)
28
+ .marginInlineStart('-0.5rem')
29
+ .marginInlineEnd('-0.5rem')
30
+ styleBuilder
31
+ .select('li.nav__item .icon', theme)
32
+ .width('1.3em')
33
+ .height('1.3em')
34
+ styleBuilder.select('li.nav__item .btn', theme).gap('0.5em').padding('0.5em')
35
+ styleBuilder
36
+ .select('.nav__list .nav__list li.nav__item', theme)
37
+ .marginInlineStart('0.75rem')
38
+ .marginInlineEnd('0rem')
39
+ styleBuilder
40
+ .select('.nav__menu', theme)
41
+ .display('block')
42
+ .padding('1em')
43
+ .borderRadius(`${palette.radii.lg} !important`)
44
+ .border('1px solid transparent')
45
+ .apply(palette.applyFont(palette.font.size.body))
46
+ .maxHeight(`calc(100vh - ${docLayoutVar('sidebarTop')} - 1.5625rem)`)
47
+ .overflowY('auto')
48
+ .overflowX('hidden')
49
+ .marginRight(docLayoutVar('activeShellPaddingInlineStart'))
50
+ styleBuilder
51
+ .select('.nav__header-row', theme)
52
+ .display('flex')
53
+ .alignItems('center')
54
+ .justifyContent('space-between')
55
+ .gap('0.5em')
56
+ registerNavAccountPlacementStyles(theme)
57
+ styleBuilder.select('.nav__search', theme).display('none').width('100%')
58
+ styleBuilder
59
+ .select('.nav__search', theme)
60
+ .media(mediaMax(BREAKPOINTS.sm))
61
+ .display('block')
62
+ styleBuilder
63
+ .select('.nav__header', theme)
64
+ .apply(palette.applyFont(palette.font.size.xxs, palette.font.weight.w700))
65
+ .textTransform('uppercase')
66
+ .color(palette.current.text.subtle)
67
+ .opacity('0.5')
68
+ styleBuilder
69
+ .select('.nav__panel-toggle', theme)
70
+ .display('none')
71
+ .alignItems('center')
72
+ .justifyContent('center')
73
+ .padding('0')
74
+ .borderRadius(palette.radii.md)
75
+ .cursor('pointer')
76
+ .transition('background 160ms ease, color 160ms ease')
77
+ .bottom('5rem')
78
+ styleBuilder.select('.nav__panel-toggle-label', theme).display('inline-block')
79
+ styleBuilder.select('.nav__panel-toggle-icon', theme).display('none')
80
+ styleBuilder
81
+ .select(
82
+ '.nav__panel-toggle:focus-visible, .nav__collapse-toggle:focus-visible',
83
+ theme,
84
+ )
85
+ .outline(`2px solid ${palette.current.border.focus}`)
86
+ styleBuilder
87
+ .select('.nav__collapse-toggle', theme)
88
+ .display('none')
89
+ .alignItems('center')
90
+ .justifyContent('center')
91
+ .padding('0.25em')
92
+ .border(`transparent`)
93
+ .borderRadius(palette.radii.md)
94
+ .background('transparent')
95
+ .cursor('pointer')
96
+ .transition('background 160ms ease, color 160ms ease')
97
+ .apply(palette.applyFont(palette.font.size.xxxs))
98
+ // Collapsing belongs to the doc layout, so only a docked menu offers it.
99
+ styleBuilder
100
+ .select('.doc-sidebar .nav__collapse-toggle', theme)
101
+ .display('inline-flex')
102
+ styleBuilder
103
+ .select('.nav__collapse-toggle-icon', theme)
104
+ .display('inline-flex')
105
+ .alignItems('center')
106
+ .justifyContent('center')
107
+ styleBuilder.select('.nav__collapse-toggle-icon--open', theme).display('none')
108
+ styleBuilder
109
+ .select(
110
+ '.template-doc--nav-collapsed .nav__collapse-toggle-icon--collapse',
111
+ theme,
112
+ )
113
+ .display('none')
114
+ styleBuilder
115
+ .select(
116
+ '.template-doc--nav-collapsed .nav__collapse-toggle-icon--open',
117
+ theme,
118
+ )
119
+ .display('inline-flex')
120
+ styleBuilder
121
+ .select('.nav__collapse-toggle-icon svg', theme)
122
+ .display('block')
123
+ .fill('none')
124
+ .stroke('currentColor')
125
+ .strokeWidth('1.9')
126
+ .strokeLinecap('round')
127
+ .strokeLinejoin('round')
128
+
129
+ styleBuilder
130
+ .select(
131
+ '.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-sidebar',
132
+ theme,
133
+ )
134
+ .media(mediaMin(BREAKPOINTS.lg))
135
+ .transformOrigin('left center')
136
+ .transform('translateX(0)')
137
+ .zIndex(30)
138
+ styleBuilder
139
+ .select(
140
+ '.template-doc.template-doc--nav-ready.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-sidebar',
141
+ theme,
142
+ )
143
+ .media(mediaMin(BREAKPOINTS.lg))
144
+ .transition('transform 260ms ease')
145
+ styleBuilder
146
+ .select(
147
+ '.template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-sidebar',
148
+ theme,
149
+ )
150
+ .media(mediaMin(BREAKPOINTS.lg))
151
+ .width(docLayoutVar('activeRailWidth'))
152
+ .position('fixed')
153
+ .left('0')
154
+ .right('auto')
155
+ .top(docLayoutVar('sidebarTop'))
156
+ .height(`calc(100dvh - ${docLayoutVar('sidebarTop')})`)
157
+ .maxHeight('none')
158
+ .background('transparent')
159
+ .boxShadow('none')
160
+ .overflow('visible')
161
+ .transform(`translateX(calc(-100% + ${docLayoutVar('activeRailWidth')}))`)
162
+ .zIndex(140)
163
+ styleBuilder
164
+ .select(
165
+ '.template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-sidebar.doc-sidebar--open',
166
+ theme,
167
+ )
168
+ .media(mediaMin(BREAKPOINTS.lg))
169
+ .width(docLayoutVar('preferredNavWidth'))
170
+ .background('transparent')
171
+ .boxShadow('none')
172
+ .transform('translateX(0)')
173
+ styleBuilder
174
+ .select(
175
+ '.template-doc.template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-shell',
176
+ theme,
177
+ )
178
+ .media(mediaMin(BREAKPOINTS.lg))
179
+ .gridTemplateColumns('minmax(0, 1fr)')
180
+ styleBuilder
181
+ .select(
182
+ '.template-doc.template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-shell--toc',
183
+ theme,
184
+ )
185
+ .media(mediaAbove(BREAKPOINTS.toc))
186
+ .gridTemplateColumns(`minmax(0, 1fr) ${docLayoutVar('activeTocWidth')}`)
187
+ styleBuilder
188
+ .select(
189
+ '.template-doc.template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-shell--toc',
190
+ theme,
191
+ )
192
+ .media(mediaMax(BREAKPOINTS.toc))
193
+ .gridTemplateColumns('1fr')
194
+ styleBuilder
195
+ .select(
196
+ '.template-doc.template-doc--nav-collapsed.template-doc--toc-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-shell--toc',
197
+ theme,
198
+ )
199
+ .media(mediaMin(BREAKPOINTS.lg))
200
+ .gridTemplateColumns('1fr')
201
+ styleBuilder
202
+ .select(
203
+ '.template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-sidebar:not(.doc-sidebar--open) .nav__header, .template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-sidebar:not(.doc-sidebar--open) .nav__collapse-toggle, .template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-sidebar:not(.doc-sidebar--open) .nav__list',
204
+ theme,
205
+ )
206
+ .media(mediaMin(BREAKPOINTS.lg))
207
+ .display('none')
208
+ styleBuilder
209
+ .select(
210
+ '.template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-sidebar:not(.doc-sidebar--open) .nav__menu',
211
+ theme,
212
+ )
213
+ .media(mediaMin(BREAKPOINTS.lg))
214
+ .width(docLayoutVar('activeRailWidth'))
215
+ .marginLeft('0')
216
+ .padding('0')
217
+ .background('transparent')
218
+ .border('none !important')
219
+ .borderRadius('0 !important')
220
+ .overflow('visible')
221
+ styleBuilder
222
+ .select(
223
+ '.template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-sidebar:not(.doc-sidebar--open) .nav__header-row',
224
+ theme,
225
+ )
226
+ .media(mediaMin(BREAKPOINTS.lg))
227
+ .margin('0')
228
+ .width(docLayoutVar('activeRailWidth'))
229
+ .height('7.375rem')
230
+ .position('static')
231
+ styleBuilder
232
+ .select(
233
+ '.template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-sidebar:not(.doc-sidebar--open) .nav__panel-toggle',
234
+ theme,
235
+ )
236
+ .media(mediaMin(BREAKPOINTS.lg))
237
+ .display('inline-flex')
238
+ .position('absolute')
239
+ .alignItems('center')
240
+ .justifyContent('center')
241
+ .writingMode('vertical-rl')
242
+ .textOrientation('mixed')
243
+ .height('auto')
244
+ .padding('0.6em 0')
245
+ .width(docLayoutVar('activeRailWidth'))
246
+ .apply(palette.applyFont(palette.font.size.xxxs, palette.font.weight.w700))
247
+ .textTransform('uppercase')
248
+ .userSelect('none')
249
+ .border(`1px solid ${palette.current.border.default}`)
250
+ .borderLeft('none')
251
+ .borderRadius(`0 ${palette.radii.md} ${palette.radii.md} 0`)
252
+ styleBuilder
253
+ .select(
254
+ '.template-doc--nav-collapsed.template-doc--has-nav:not(.template-doc--nav-drawer) .doc-sidebar.doc-sidebar--open .nav__panel-toggle',
255
+ theme,
256
+ )
257
+ .media(mediaMin(BREAKPOINTS.lg))
258
+ .display('none')
259
+
260
+ styleBuilder
261
+ .select(
262
+ '.doc-sidebar .nav__panel-toggle, .doc-sidebar .nav__collapse-toggle',
263
+ theme,
264
+ )
265
+ .media(mediaBelow(BREAKPOINTS.lg))
266
+ .display('none')
267
+ styleBuilder
268
+ .select(
269
+ '.template-doc--nav-drawer .nav__panel-toggle, .template-doc--nav-drawer .nav__collapse-toggle',
270
+ theme,
271
+ )
272
+ .display('none')
273
+ styleBuilder
274
+ .select('.nav__list', theme)
275
+ .listStyle('none')
276
+ .margin('0')
277
+ .padding('0')
278
+ styleBuilder
279
+ .select('.template-doc--has-nav .doc-sidebar .nav__menu', theme)
280
+ .media(mediaBelow(BREAKPOINTS.lg))
281
+ .width('100%')
282
+ .maxWidth('none')
283
+ .minHeight(`calc(100dvh - ${docLayoutVar('sidebarTopMobile')})`)
284
+ .maxHeight('none')
285
+ .borderRadius('0 !important')
286
+ .border('0 !important')
287
+ .padding('1rem 1rem 1.25rem')
288
+ .height('100%')
289
+ .overflow('auto')
290
+ .boxSizing('border-box')
291
+ }
292
+
293
+ function registerNavAccountPlacementStyles(theme: ThemeMode) {
294
+ styleBuilder.select('.nav__menu .nav__account', theme).display('none')
295
+ for (const rule of getTopBarToggleVisibleSelectors(
296
+ '.doc-sidebar .nav__menu .nav__account',
297
+ )) {
298
+ let selector = styleBuilder.select(rule.selector, theme)
299
+ if (rule.media) selector = selector.media(rule.media)
300
+ selector.display('inline-block').marginLeft('auto')
301
+ }
302
+ }
303
+
304
+ export function registerNavSummaryStyles(
305
+ theme: ThemeMode,
306
+ palette: ThemePalette,
307
+ ) {
308
+ styleBuilder.select('.nav__summary', theme).position('relative')
309
+ styleBuilder
310
+ .select('.nav__chevron', theme)
311
+ .position('absolute')
312
+ .left('auto')
313
+ .right('0.125rem')
314
+ .transform('rotate(0deg)')
315
+ .transition('transform 160ms ease')
316
+ .top('50%')
317
+ .set('translate', '0 -50%')
318
+ .color(palette.current.text.subtle)
319
+ styleBuilder
320
+ .select('.nav__group[open] > .nav__summary .nav__chevron', theme)
321
+ .transform('rotate(-90deg)')
322
+ styleBuilder.select('.nav__summary::marker', theme).content('""')
323
+ }
@@ -0,0 +1,90 @@
1
+ import type {
2
+ PageNavigationLink,
3
+ PageNavigationLinks,
4
+ TsSsgContext,
5
+ } from '@purestack/ts-common'
6
+ import { resolveTsSsgContext } from '@purestack/ts-common'
7
+ import { defineComponent, html } from 'regor'
8
+
9
+ export interface PageLinks extends PageNavigationLinks {
10
+ hasLinks?: boolean
11
+ }
12
+
13
+ const pageLinksTemplate = html`<Grid
14
+ r-if="hasLinks"
15
+ container="nav"
16
+ columns="2"
17
+ class="pt-4 bt-1 b-subtle"
18
+ aria-label="Adjacent pages"
19
+ >
20
+ <Flex class="min-w-0" justify="start">
21
+ <BtnLink
22
+ r-if="previous"
23
+ :href="previous.url"
24
+ tone="neutral"
25
+ variant="outline"
26
+ size="sm"
27
+ icon="iconoir:nav-arrow-left"
28
+ iconPosition="start"
29
+ rel="prev"
30
+ class="w-full ws-normal text-start min-w-0 justify-start"
31
+ :ariaLabel="'Previous: ' + previous.title"
32
+ >
33
+ <span class="d-inline-flex flex-column align-flex-start min-w-0">
34
+ <span class="text-eyebrow fs-xxxs">Previous</span>
35
+ <span class="min-w-0 lh-2">{{ previous.title }}</span>
36
+ </span>
37
+ </BtnLink>
38
+ </Flex>
39
+ <Flex class="min-w-0" justify="start" justifySm="end">
40
+ <BtnLink
41
+ r-if="next"
42
+ :href="next.url"
43
+ tone="neutral"
44
+ variant="outline"
45
+ size="sm"
46
+ icon="iconoir:nav-arrow-right"
47
+ iconPosition="end"
48
+ rel="next"
49
+ class="w-full ws-normal text-end min-w-0 justify-end"
50
+ :ariaLabel="'Next: ' + next.title"
51
+ >
52
+ <span class="d-inline-flex flex-column align-flex-end min-w-0">
53
+ <span class="text-eyebrow fs-xxxs">Next</span>
54
+ <span class="min-w-0 lh-2">{{ next.title }}</span>
55
+ </span>
56
+ </BtnLink>
57
+ </Flex>
58
+ </Grid>`
59
+
60
+ function resolvePageLinks(
61
+ props: PageLinks,
62
+ context: TsSsgContext | undefined,
63
+ ): PageLinks {
64
+ const previous = props.previous ?? context?.navigation?.pageLinks?.previous
65
+ const next = props.next ?? context?.navigation?.pageLinks?.next
66
+ const resolvedPrevious = resolvePageLink(previous)
67
+ const resolvedNext = resolvePageLink(next)
68
+ return {
69
+ previous: resolvedPrevious,
70
+ next: resolvedNext,
71
+ hasLinks: Boolean(resolvedPrevious || resolvedNext),
72
+ }
73
+ }
74
+
75
+ function resolvePageLink(link: PageNavigationLink | undefined) {
76
+ if (!link?.url || !link.title) return undefined
77
+ return link
78
+ }
79
+
80
+ function definePageLinksComponent() {
81
+ return defineComponent<PageLinks>(pageLinksTemplate, {
82
+ props: ['previous', 'next'],
83
+ context: (head) => resolvePageLinks(head.props, resolveTsSsgContext(head)),
84
+ })
85
+ }
86
+
87
+ export function definePageLinksComponents() {
88
+ const pageLinks = definePageLinksComponent()
89
+ return { pageLinks }
90
+ }
@@ -0,0 +1,155 @@
1
+ import { resolveTsSsgContext } from '@purestack/ts-common'
2
+ import {
3
+ dirnamePosix,
4
+ joinPosix,
5
+ normalizePosixPath,
6
+ toOutputAssetRelPath,
7
+ toPosixPath,
8
+ urlNormalizer,
9
+ } from '@purestack/ts-util'
10
+ import { type ComponentHead, defineComponent, flatten, html } from 'regor'
11
+
12
+ export interface PageScript {
13
+ teleport?: string
14
+ src?: string
15
+ sourceRelPath?: string
16
+ type?: string
17
+ async?: unknown
18
+ defer?: unknown
19
+ integrity?: string
20
+ nonce?: string
21
+ crossOrigin?: string
22
+ referrerPolicy?: string
23
+ noModule?: unknown
24
+ }
25
+
26
+ export interface RegorApp {
27
+ src?: string
28
+ sourceRelPath?: string
29
+ }
30
+
31
+ const pageScriptTemplate = html`<script
32
+ :r-teleport="teleport"
33
+ :src="src"
34
+ :type="type"
35
+ :async="async"
36
+ :defer="defer"
37
+ :integrity="integrity"
38
+ :nonce="nonce"
39
+ :crossorigin="crossOrigin"
40
+ :referrerpolicy="referrerPolicy"
41
+ :nomodule="noModule"
42
+ ></script>`
43
+
44
+ const regorAppTemplate = html`<App><PageScript
45
+ :src="src"
46
+ :sourceRelPath="sourceRelPath"
47
+ /></App>`
48
+
49
+ function definePageScriptComponent() {
50
+ return defineComponent<PageScript>(pageScriptTemplate, {
51
+ props: [
52
+ 'teleport',
53
+ 'src',
54
+ 'sourceRelPath',
55
+ 'type',
56
+ 'async',
57
+ 'defer',
58
+ 'integrity',
59
+ 'nonce',
60
+ 'crossOrigin',
61
+ 'referrerPolicy',
62
+ 'noModule',
63
+ ],
64
+ context: (head) => resolvePageScript(head),
65
+ })
66
+ }
67
+
68
+ function defineRegorAppComponent() {
69
+ return defineComponent<RegorApp>(regorAppTemplate, {
70
+ props: ['src', 'sourceRelPath'],
71
+ context: (head) => ({
72
+ src: head.props.src,
73
+ sourceRelPath: head.props.sourceRelPath,
74
+ }),
75
+ })
76
+ }
77
+
78
+ export function defineScriptComponents() {
79
+ return {
80
+ pageScript: definePageScriptComponent(),
81
+ regorApp: defineRegorAppComponent(),
82
+ }
83
+ }
84
+
85
+ function resolvePageScript(head: ComponentHead<PageScript>): PageScript {
86
+ const props = flatten(head.props)
87
+ const tsSsgContext = resolveTsSsgContext(head)
88
+ const ownerRelPath =
89
+ toOptionalString(props.sourceRelPath) ?? tsSsgContext.pageInfo.relPath
90
+ const src = resolveScriptSrc(
91
+ props.src,
92
+ ownerRelPath,
93
+ (sourceRelPath) => {
94
+ tsSsgContext.recordScriptEntrypoint(sourceRelPath)
95
+ },
96
+ tsSsgContext.resolveScriptPublicPath,
97
+ )
98
+ return {
99
+ ...props,
100
+ src,
101
+ type: toOptionalString(props.type) ?? 'module',
102
+ teleport: toOptionalString(props.teleport) ?? 'body',
103
+ }
104
+ }
105
+
106
+ function resolveScriptSrc(
107
+ src: unknown,
108
+ pageRelPath: string,
109
+ onSourceResolved?: (sourceRelPath: string) => void,
110
+ resolveScriptPublicPath?: (sourceRelPath: string) => string,
111
+ ) {
112
+ const normalized = toOptionalString(src)
113
+ if (!normalized) {
114
+ throw new Error('PageScript requires a non-empty "src" prop.')
115
+ }
116
+ if (isExternalSrc(normalized)) return normalized
117
+ const { base, suffix } = urlNormalizer.splitSuffix(normalized)
118
+ const sourceRelPath = resolveSourceRelPath(base, pageRelPath)
119
+ onSourceResolved?.(sourceRelPath)
120
+ const publicPath =
121
+ resolveScriptPublicPath?.(sourceRelPath) ??
122
+ `/${toOutputAssetRelPath(sourceRelPath)}`
123
+ return `${publicPath}${suffix}`
124
+ }
125
+
126
+ function resolveSourceRelPath(value: string, pageRelPath: string) {
127
+ if (value.startsWith('/')) {
128
+ return trimLeadingSlashes(value)
129
+ }
130
+ const sourceDir = dirnamePosix(toPosixPath(pageRelPath))
131
+ const normalizedSourceDir = sourceDir === '.' ? '' : sourceDir
132
+ const resolved = normalizePosixPath(joinPosix(normalizedSourceDir, value))
133
+ if (resolved === '..' || resolved.startsWith('../')) {
134
+ throw new Error(`PageScript src resolves outside content root: "${value}"`)
135
+ }
136
+ return resolved
137
+ }
138
+
139
+ function isExternalSrc(value: string) {
140
+ return (
141
+ value.startsWith('//') ||
142
+ value.startsWith('http://') ||
143
+ value.startsWith('https://')
144
+ )
145
+ }
146
+
147
+ function trimLeadingSlashes(value: string) {
148
+ return value.replace(/^\/+/, '')
149
+ }
150
+
151
+ function toOptionalString(value: unknown) {
152
+ if (value === null || value === undefined) return undefined
153
+ const trimmed = String(value).trim()
154
+ return trimmed.length > 0 ? trimmed : undefined
155
+ }