@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,70 @@
1
+ import {
2
+ styleBuilder,
3
+ type ThemeMode,
4
+ type ThemePalette,
5
+ themes,
6
+ } from '@purestack/ts-style'
7
+
8
+ export function registerSignInStyles() {
9
+ themes.forEach((theme, palette) => {
10
+ registerSignInDisclosureStyles(theme)
11
+ registerSignInAuthStateStyles(theme)
12
+ registerSignInPanelPlacementStyles(theme)
13
+ registerSignInAvatarStyles(theme, palette)
14
+ })
15
+ }
16
+
17
+ function registerSignInDisclosureStyles(theme: ThemeMode) {
18
+ styleBuilder.select('.sign-in', theme).display('inline-block').lineHeight('0')
19
+ }
20
+
21
+ function registerSignInAuthStateStyles(theme: ThemeMode) {
22
+ styleBuilder
23
+ .select('.sign-in__signed-out-view, .sign-in__signed-in-view', theme)
24
+ .display('inline-flex')
25
+ .alignItems('center')
26
+ .justifyContent('center')
27
+ .width('100%')
28
+ .height('100%')
29
+
30
+ styleBuilder
31
+ .select(
32
+ '.sign-in__signed-in-view, :scope.signed-in .sign-in__signed-out-view',
33
+ theme,
34
+ )
35
+ .display('none')
36
+
37
+ styleBuilder
38
+ .select(':scope.signed-in .sign-in__signed-in-view', theme)
39
+ .display('inline-flex')
40
+
41
+ styleBuilder
42
+ .select(
43
+ '.sign-in__signed-in-action, :scope.signed-in .sign-in__signed-out-action',
44
+ theme,
45
+ )
46
+ .display('none')
47
+
48
+ styleBuilder
49
+ .select(':scope.signed-in .sign-in__signed-in-action', theme)
50
+ .display('inline-flex')
51
+ }
52
+
53
+ function registerSignInPanelPlacementStyles(theme: ThemeMode) {
54
+ styleBuilder
55
+ .select('.sign-in__panel', theme)
56
+ .position('absolute')
57
+ .display('block')
58
+ .top('100%')
59
+ .right('0')
60
+ .zIndex(60)
61
+ .minWidth('11.25rem')
62
+ }
63
+
64
+ function registerSignInAvatarStyles(theme: ThemeMode, palette: ThemePalette) {
65
+ styleBuilder
66
+ .select('.sign-in__avatar', theme)
67
+ .display('block')
68
+ .objectFit('cover')
69
+ .borderRadius(palette.radii.pill)
70
+ }
@@ -0,0 +1,193 @@
1
+ import { tryResolveTsSsgContext } from '@purestack/ts-common'
2
+ import type { SemanticTone } from '@purestack/ts-style'
3
+ import {
4
+ type ComponentHead,
5
+ type ComputedRef,
6
+ computed,
7
+ defineComponent,
8
+ html,
9
+ isRef,
10
+ type RefOrValue,
11
+ ref,
12
+ unref,
13
+ } from 'regor'
14
+ import { createAutoId } from '../autoId'
15
+ import {
16
+ type ComponentVariant,
17
+ type ComponentVariantMode,
18
+ resolveComponentClasses,
19
+ } from '../componentVariant'
20
+
21
+ const defaultGroup = 'tabs-default'
22
+ const DEFAULT_TABS_VARIANT_MODE: ComponentVariantMode = 'stateless'
23
+ const resolveTabGroup = createAutoId(defaultGroup)
24
+ const resolveTabId = createAutoId('tab')
25
+
26
+ export class Tabs {
27
+ group: string = defaultGroup
28
+ ariaLabel?: RefOrValue<string>
29
+ selectedTab?: RefOrValue<string>
30
+ tone?: RefOrValue<SemanticTone>
31
+ variant?: RefOrValue<ComponentVariant>
32
+ variantMode?: RefOrValue<ComponentVariantMode>
33
+ tabVariant?: RefOrValue<ComponentVariant>
34
+ classes?: ComputedRef<string>
35
+ }
36
+
37
+ export interface TabPane {
38
+ id?: string
39
+ label?: RefOrValue<string>
40
+ icon?: RefOrValue<string>
41
+ disabled?: RefOrValue<boolean>
42
+ group?: string
43
+ tone?: RefOrValue<SemanticTone>
44
+ variant?: RefOrValue<ComponentVariant>
45
+ variantMode?: RefOrValue<ComponentVariantMode>
46
+ tabVariant?: RefOrValue<ComponentVariant>
47
+ classes?: ComputedRef<string>
48
+ tabClasses?: ComputedRef<string>
49
+ }
50
+
51
+ const tabsTemplate = html`<section class="tabs" :class="classes">
52
+ <slot name="header"></slot>
53
+ <div class="tabs__list" role="tablist" :aria-label="ariaLabel || 'Tabs'">
54
+ <slot></slot>
55
+ </div>
56
+ </section>`
57
+
58
+ const tabPaneTemplate = html`<div class="tabs__item">
59
+ <input
60
+ class="tabs__control"
61
+ type="radio"
62
+ :name="group"
63
+ :id="id"
64
+ :value="id"
65
+ r-model="selectedTab"
66
+ :disabled="disabled"/>
67
+ <label
68
+ class="tabs__tab"
69
+ :class="tabClasses"
70
+ role="tab"
71
+ :id="id+'-label'"
72
+ :for="id"
73
+ :aria-controls="id+'-panel'"
74
+ :aria-selected="selectedTab === id"
75
+ :aria-disabled="disabled"
76
+ >
77
+ <Icon class="tabs__tab-icon" :name="icon"/>
78
+ <span class="tabs__tab-label">{{ label || id }}</span>
79
+ </label>
80
+ <section
81
+ class="tabs__panel"
82
+ :class="classes"
83
+ role="tabpanel"
84
+ :id="id+'-panel'"
85
+ :aria-labelledby="id+'-label'"
86
+ >
87
+ <div class="tabs__panel-body" :class="class">
88
+ <slot></slot>
89
+ </div>
90
+ </section>
91
+ </div>`
92
+
93
+ function defineTabsComponent() {
94
+ return defineComponent<Tabs>(tabsTemplate, {
95
+ props: [
96
+ 'ariaLabel',
97
+ 'group',
98
+ 'selectedTab',
99
+ 'tone',
100
+ 'variant',
101
+ 'variantMode',
102
+ 'tabVariant',
103
+ ],
104
+ context: (head) => {
105
+ markTabsRuntimeEmbed(head)
106
+ return resolveTabs(head.props)
107
+ },
108
+ })
109
+ }
110
+
111
+ function defineTabPaneComponent() {
112
+ return defineComponent<TabPane>(tabPaneTemplate, {
113
+ props: [
114
+ 'id',
115
+ 'label',
116
+ 'icon',
117
+ 'disabled',
118
+ 'group',
119
+ 'tone',
120
+ 'variant',
121
+ 'variantMode',
122
+ 'tabVariant',
123
+ ],
124
+ context: (head) => resolveTabPane(head),
125
+ })
126
+ }
127
+
128
+ export function defineTabsComponents() {
129
+ return {
130
+ tabs: defineTabsComponent(),
131
+ tabPane: defineTabPaneComponent(),
132
+ }
133
+ }
134
+
135
+ function resolveTabs(props: Tabs): Tabs {
136
+ const group = resolveTabGroup(unref(props.group))
137
+ const tabs = new Tabs()
138
+ const selectedTab = ensureSelectedTabIsRef(props.selectedTab)
139
+ return Object.assign(tabs, {
140
+ ...props,
141
+ group,
142
+ selectedTab,
143
+ classes: computed(() =>
144
+ resolveComponentClasses(props, {
145
+ defaultVariant: 'surface',
146
+ defaultVariantMode: DEFAULT_TABS_VARIANT_MODE,
147
+ }),
148
+ ),
149
+ })
150
+ }
151
+
152
+ function ensureSelectedTabIsRef(selectedTab?: RefOrValue<string>) {
153
+ return isRef(selectedTab) ? selectedTab : ref(selectedTab)
154
+ }
155
+
156
+ function resolveTabPane(head: ComponentHead<TabPane>): TabPane {
157
+ const fromParent = head.findContext(Tabs)
158
+ const group = head.props.group || fromParent?.group || defaultGroup
159
+ const id = resolveTabId(unref(head.props.id))
160
+ const tone = head.props.tone || fromParent?.tone
161
+ const variantMode = head.props.variantMode || fromParent?.variantMode
162
+ const tabVariant = head.props.tabVariant || fromParent?.tabVariant
163
+ return {
164
+ ...head.props,
165
+ id,
166
+ group,
167
+ tone,
168
+ variantMode,
169
+ tabVariant,
170
+ classes: computed(() =>
171
+ resolveComponentClasses(
172
+ { tone, variant: head.props.variant, variantMode },
173
+ {
174
+ defaultVariant: 'none',
175
+ defaultVariantMode: DEFAULT_TABS_VARIANT_MODE,
176
+ },
177
+ ),
178
+ ),
179
+ tabClasses: computed(() =>
180
+ resolveComponentClasses(
181
+ { tone, variant: tabVariant },
182
+ {
183
+ defaultVariant: 'underline',
184
+ classes: [unref(head.props.disabled) ? 'tabs__tab--disabled' : ''],
185
+ },
186
+ ),
187
+ ),
188
+ }
189
+ }
190
+
191
+ function markTabsRuntimeEmbed(head: ComponentHead<Tabs>) {
192
+ tryResolveTsSsgContext(head)?.recordRuntimeEmbed('tabs', 'head')
193
+ }
@@ -0,0 +1,231 @@
1
+ import {
2
+ styleBuilder,
3
+ type ThemeMode,
4
+ type ThemePalette,
5
+ themes,
6
+ } from '@purestack/ts-style'
7
+
8
+ export function registerTabsStyles() {
9
+ themes.forEach((theme, palette) => {
10
+ registerTabsShellStyles(theme, palette)
11
+ registerTabsControlStyles(theme, palette)
12
+ registerTabsPanelStyles(theme)
13
+ registerTabsResponsiveStyles(theme)
14
+ })
15
+ }
16
+
17
+ function registerTabsShellStyles(theme: ThemeMode, palette: ThemePalette) {
18
+ styleBuilder
19
+ .select('.tabs', theme)
20
+ .display('grid')
21
+ .gap('0.6em')
22
+ .alignContent('start')
23
+ .padding('1em')
24
+ .boxShadow(palette.effect.softShadow)
25
+
26
+ styleBuilder
27
+ .select('.tabs__list', theme)
28
+ .display('grid')
29
+ .gridTemplateColumns('repeat(auto-fit, minmax(7.5rem, 1fr))')
30
+ .gap('0.6em 0.4em')
31
+ .alignItems('stretch')
32
+ .alignContent('start')
33
+ .minWidth('0')
34
+
35
+ styleBuilder.select('.tabs__item', theme).display('contents')
36
+
37
+ styleBuilder
38
+ .select('.tabs__tabs-row', theme)
39
+ .display('none')
40
+ .alignItems('center')
41
+ .gap('0.5em')
42
+ .minWidth('0')
43
+
44
+ styleBuilder
45
+ .select('.tabs__tab-buttons', theme)
46
+ .display('flex')
47
+ .alignItems('center')
48
+ .gap('0.5em')
49
+ .minWidth('0')
50
+ .flex('1')
51
+ .overflow('hidden')
52
+
53
+ styleBuilder
54
+ .select('.tabs__overflow', theme)
55
+ .display('none')
56
+ .position('relative')
57
+ .flexShrink('0')
58
+
59
+ styleBuilder.select('.tabs__overflow--visible', theme).display('inline-flex')
60
+
61
+ styleBuilder
62
+ .select('.tabs__overflow-toggle', theme)
63
+ .width('2.3em')
64
+ .height('2.3em')
65
+ .padding('0')
66
+
67
+ styleBuilder
68
+ .select('.tabs__overflow-menu', theme)
69
+ .display('none')
70
+ .position('absolute')
71
+ .right('0')
72
+ .top('calc(100% + 0.375rem)')
73
+ .zIndex(20)
74
+ .minWidth('100%')
75
+ .width('max-content')
76
+ .maxWidth('min(92vw, 28.75rem)')
77
+ .padding('0.375rem')
78
+ .borderRadius(palette.radii.md)
79
+ .border('1px solid transparent')
80
+ .boxShadow(palette.effect.softShadow)
81
+
82
+ styleBuilder
83
+ .select('.tabs__overflow--open .tabs__overflow-menu', theme)
84
+ .display('grid')
85
+ .gap('0.25rem')
86
+
87
+ styleBuilder
88
+ .select('.tabs__overflow-option', theme)
89
+ .width('100%')
90
+ .padding('0.4em 0.6em')
91
+ .textAlign('left')
92
+ .whiteSpace('nowrap')
93
+ .apply(palette.applyFont(palette.font.size.xxs, palette.font.weight.w600))
94
+
95
+ styleBuilder.select('.tabs__overflow-option:hover', theme)
96
+
97
+ styleBuilder
98
+ .select('.tabs__select-wrap', theme)
99
+ .display('none')
100
+ .margin('0.125rem 0 0.25rem')
101
+
102
+ styleBuilder
103
+ .select('.tabs__select', theme)
104
+ .display('block')
105
+ .width('100%')
106
+ .minHeight('2.625rem')
107
+ .padding('0.625rem 2.5rem 0.625rem 0.75rem')
108
+ .borderRadius(palette.radii.md)
109
+ .border('1px solid transparent')
110
+ .apply(palette.applyFont(palette.font.size.xs, palette.font.weight.w600))
111
+ .appearance('none')
112
+ .webkitAppearance('none')
113
+ .mozAppearance('none')
114
+ }
115
+
116
+ function registerTabsControlStyles(theme: ThemeMode, palette: ThemePalette) {
117
+ styleBuilder
118
+ .select('.tabs__control', theme)
119
+ .position('absolute')
120
+ .width('1px')
121
+ .height('1px')
122
+ .opacity('0')
123
+ .pointerEvents('none')
124
+
125
+ styleBuilder
126
+ .select('.tabs__tab, .tabs__tab-buttons > .btn', theme)
127
+ .display('inline-flex')
128
+ .alignItems('center')
129
+ .justifyContent('center')
130
+ .padding('0.5em 0.75em')
131
+ .apply(palette.applyFont(palette.font.size.xxs, palette.font.weight.w700))
132
+ .gap('0.5em')
133
+ .overflow('hidden')
134
+ .textAlign('center')
135
+ .cursor('pointer')
136
+ .transition(
137
+ 'background 160ms ease, color 160ms ease, border-color 160ms ease',
138
+ )
139
+
140
+ styleBuilder.select('.tabs__tab', theme).gridRow('1')
141
+
142
+ styleBuilder
143
+ .select('.tabs__tab-buttons > .btn', theme)
144
+ .flexShrink('0')
145
+ .whiteSpace('nowrap')
146
+
147
+ styleBuilder.select('.tabs__tab-buttons > .btn:hover', theme)
148
+
149
+ styleBuilder.select('.tabs__tab-buttons > .btn.active', theme)
150
+
151
+ styleBuilder
152
+ .select('.tabs__tab-icon', theme)
153
+ .display('inline-flex')
154
+ .alignItems('center')
155
+ .justifyContent('center')
156
+ .width('1.7em')
157
+ .height('1.7em')
158
+ .flexShrink('0')
159
+
160
+ styleBuilder
161
+ .select('.tabs__tab-icon svg', theme)
162
+ .display('block')
163
+ .width('100%')
164
+ .height('100%')
165
+
166
+ styleBuilder.select('.tabs__tab-label', theme).display('inline-block')
167
+ styleBuilder
168
+ .select('.tabs__tab-label', theme)
169
+ .overflow('hidden')
170
+ .textOverflow('ellipsis')
171
+ .whiteSpace('nowrap')
172
+ .maxWidth('100%')
173
+
174
+ styleBuilder
175
+ .select('.tabs__control:disabled + .tabs__tab, .tabs__tab--disabled', theme)
176
+ .opacity('0.45')
177
+ .cursor('not-allowed')
178
+
179
+ styleBuilder
180
+ .select('.tabs__tab-buttons > .btn:disabled', theme)
181
+ .opacity('0.45')
182
+ .cursor('not-allowed')
183
+
184
+ styleBuilder
185
+ .select('.tabs__control:focus-visible + .tabs__tab', theme)
186
+ .outline('2px solid currentColor')
187
+ .outlineOffset('2px')
188
+
189
+ styleBuilder
190
+ .select('.tabs__tab-buttons > .btn[hidden]', theme)
191
+ .display('none')
192
+ }
193
+
194
+ function registerTabsPanelStyles(theme: ThemeMode) {
195
+ styleBuilder
196
+ .select('.tabs__panel', theme)
197
+ .gridRow('2')
198
+ .gridColumn('1 / -1')
199
+ .display('none')
200
+ .minWidth('0')
201
+ .padding('0')
202
+
203
+ styleBuilder
204
+ .select('.tabs__control:checked + .tabs__tab + .tabs__panel', theme)
205
+ .display('block')
206
+
207
+ styleBuilder
208
+ .select(
209
+ '.tabs__list:not(:has(.tabs__control:checked)) .tabs__item:first-child .tabs__panel',
210
+ theme,
211
+ )
212
+ .display('block')
213
+
214
+ styleBuilder.select('.tabs__panel-body', theme)
215
+ }
216
+
217
+ function registerTabsResponsiveStyles(theme: ThemeMode) {
218
+ styleBuilder.select('.tabs--enhanced .tabs__tabs-row', theme).display('flex')
219
+
220
+ styleBuilder
221
+ .select('.tabs--compact .tabs__select-wrap', theme)
222
+ .display('block')
223
+
224
+ styleBuilder.select('.tabs--enhanced .tabs__tab', theme).display('none')
225
+
226
+ styleBuilder.select('.tabs--compact .tabs__tabs-row', theme).display('none')
227
+
228
+ styleBuilder
229
+ .select('.tabs--compact .tabs__list', theme)
230
+ .gridTemplateColumns('1fr')
231
+ }
@@ -0,0 +1,33 @@
1
+ import { defineComponent, html } from 'regor'
2
+
3
+ export interface ThemeToggle {}
4
+
5
+ /**
6
+ * One icon button that flips between the light and dark themes. The theme
7
+ * stylesheets decide which glyph shows, so the markup carries no state; the
8
+ * theme runtime binds every [data-theme-toggle] and keeps aria-pressed in sync.
9
+ */
10
+ const themeToggleTemplate = html`<button
11
+ class="theme-toggle"
12
+ type="button"
13
+ aria-label="Dark theme"
14
+ aria-pressed="false"
15
+ data-theme-toggle
16
+ >
17
+ <Icon
18
+ class="theme-toggle__glyph theme-toggle__glyph--light"
19
+ name="lucide:sun"
20
+ aria-hidden="true"/>
21
+ <Icon
22
+ class="theme-toggle__glyph theme-toggle__glyph--dark"
23
+ name="lucide:moon"
24
+ aria-hidden="true"/>
25
+ </button>`
26
+
27
+ function defineThemeToggleComponent() {
28
+ return defineComponent<ThemeToggle>(themeToggleTemplate)
29
+ }
30
+
31
+ export function defineThemeToggleComponents() {
32
+ return { themeToggle: defineThemeToggleComponent() }
33
+ }
@@ -0,0 +1,88 @@
1
+ import {
2
+ styleBuilder,
3
+ type ThemeMode,
4
+ type ThemePalette,
5
+ themes,
6
+ } from '@purestack/ts-style'
7
+
8
+ const GLYPH_MOTION =
9
+ 'transform 360ms cubic-bezier(0.34, 1.56, 0.64, 1), opacity 200ms ease'
10
+
11
+ export function registerThemeToggleStyles() {
12
+ themes.forEach((theme, palette) => {
13
+ registerThemeToggleButtonStyles(theme, palette)
14
+ registerThemeToggleGlyphStyles(theme)
15
+ })
16
+ }
17
+
18
+ function registerThemeToggleButtonStyles(
19
+ theme: ThemeMode,
20
+ palette: ThemePalette,
21
+ ) {
22
+ styleBuilder
23
+ .select('.theme-toggle', theme)
24
+ .display('inline-grid')
25
+ .placeItems('center')
26
+ .flexShrink('0')
27
+ .width('2.625rem')
28
+ .height('2.625rem')
29
+ .padding('0')
30
+ .borderRadius(palette.radii.pill)
31
+ .border('1px solid transparent')
32
+ .background('transparent')
33
+ .color(palette.current.text.subtle)
34
+ .cursor('pointer')
35
+ .transition('background 160ms ease, color 160ms ease')
36
+
37
+ styleBuilder
38
+ .select('.theme-toggle:hover', theme)
39
+ .background(palette.current.surface.hover.background)
40
+ .borderColor(palette.current.surface.hover.border)
41
+ .color(palette.current.text.default)
42
+
43
+ styleBuilder
44
+ .select('.theme-toggle:active', theme)
45
+ .background(palette.current.surface.active.background)
46
+
47
+ styleBuilder
48
+ .select('.theme-toggle:focus-visible', theme)
49
+ .outline(`2px solid ${palette.current.border.focus}`)
50
+ .outlineOffset('2px')
51
+ }
52
+
53
+ /**
54
+ * Both glyphs share one grid cell. The theme in use shows its own glyph and
55
+ * tucks the other away rotated, so a switch reads as one turning motion.
56
+ */
57
+ function registerThemeToggleGlyphStyles(theme: ThemeMode) {
58
+ const shown = theme === 'dark' ? 'dark' : 'light'
59
+ const hidden = theme === 'dark' ? 'light' : 'dark'
60
+ const hiddenTurn = theme === 'dark' ? '90deg' : '-90deg'
61
+
62
+ styleBuilder
63
+ .select('.theme-toggle .theme-toggle__glyph', theme)
64
+ .gridArea('1 / 1')
65
+ .width('1.25rem')
66
+ .height('1.25rem')
67
+
68
+ styleBuilder
69
+ .select(`.theme-toggle .theme-toggle__glyph--${shown}`, theme)
70
+ .opacity(1)
71
+ .transform('rotate(0deg) scale(1)')
72
+
73
+ styleBuilder
74
+ .select(`.theme-toggle .theme-toggle__glyph--${hidden}`, theme)
75
+ .opacity(0)
76
+ .transform(`rotate(${hiddenTurn}) scale(0.5)`)
77
+
78
+ // Animate only once the runtime has settled the first theme, so a page
79
+ // never loads with the glyphs mid-turn.
80
+ styleBuilder
81
+ .select(':scope[data-theme-ready] .theme-toggle__glyph', theme)
82
+ .transition(GLYPH_MOTION)
83
+
84
+ styleBuilder
85
+ .select(':scope[data-theme-ready] .theme-toggle__glyph', theme)
86
+ .media('prefers-reduced-motion: reduce')
87
+ .transition('none')
88
+ }
@@ -0,0 +1,42 @@
1
+ import { defineComponent, html, type SRef } from 'regor'
2
+ import type { ToastItem, ToastStore } from './toastStore'
3
+
4
+ export interface ToastHost {
5
+ items: SRef<ToastItem[]>
6
+ dismiss: ToastStore['dismiss']
7
+ }
8
+
9
+ export const toastHostTemplate = html`<Flex
10
+ direction="column"
11
+ align="center"
12
+ r-if="items.length > 0"
13
+ style="position: fixed; top: 1rem; left: 50%; z-index: 1200; max-width: calc(100vw - 2rem); transform: translateX(-50%); pointer-events: none;"
14
+ >
15
+ <Panel
16
+ class="py-0"
17
+ r-for="toast in items"
18
+ :tone="toast.tone"
19
+ variant="surface"
20
+ style="max-width: calc(100vw - 2rem); pointer-events: auto;"
21
+ >
22
+ <Flex align="center" justify="between" wrap="true">
23
+ <span>{{ toast.message }}</span>
24
+ <Btn
25
+ size="sm"
26
+ variant="subtleBtn"
27
+ icon="iconoir:xmark"
28
+ @click="dismiss(toast.id)"
29
+ >
30
+ Dismiss
31
+ </Btn>
32
+ </Flex>
33
+ </Panel>
34
+ </Flex>`
35
+
36
+ export function defineToastComponents() {
37
+ return {
38
+ toastHost: defineComponent<ToastHost>(toastHostTemplate, {
39
+ props: ['items', 'dismiss'],
40
+ }),
41
+ }
42
+ }
@@ -0,0 +1,50 @@
1
+ import type { SemanticTone } from '@purestack/ts-style'
2
+ import { sref } from 'regor'
3
+
4
+ export interface ToastItem {
5
+ id: string
6
+ message: string
7
+ tone: SemanticTone
8
+ }
9
+
10
+ const TOAST_TIMEOUT_MS: Partial<Record<SemanticTone, number>> = {
11
+ info: 5000,
12
+ success: 5000,
13
+ warning: 10000,
14
+ }
15
+
16
+ export class ToastStore {
17
+ readonly items = sref<ToastItem[]>([])
18
+ private nextId = 0
19
+ private readonly timers = new Map<string, ReturnType<typeof setTimeout>>()
20
+
21
+ notify = (text: string, tone: SemanticTone = 'info') => {
22
+ if (!text) return
23
+ const toast: ToastItem = {
24
+ id: `toast-${++this.nextId}`,
25
+ message: text,
26
+ tone,
27
+ }
28
+ this.items([...this.items(), toast])
29
+ const timeout = TOAST_TIMEOUT_MS[tone]
30
+ if (timeout) {
31
+ this.timers.set(
32
+ toast.id,
33
+ setTimeout(() => this.dismiss(toast.id), timeout),
34
+ )
35
+ }
36
+ }
37
+
38
+ dismiss = (id: string) => {
39
+ const timer = this.timers.get(id)
40
+ if (timer) clearTimeout(timer)
41
+ this.timers.delete(id)
42
+ this.items(this.items().filter((toast) => toast.id !== id))
43
+ }
44
+
45
+ clear = () => {
46
+ for (const timer of this.timers.values()) clearTimeout(timer)
47
+ this.timers.clear()
48
+ this.items([])
49
+ }
50
+ }