@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,205 @@
1
+ import { tryResolveTsSsgContext } from '@purestack/ts-common'
2
+ import type { SemanticTone } from '@purestack/ts-style'
3
+ import { urlNormalizer } from '@purestack/ts-util'
4
+ import {
5
+ type ComponentHead,
6
+ type ComputedRef,
7
+ computed,
8
+ defineComponent,
9
+ html,
10
+ type RefOrValue,
11
+ unref,
12
+ } from 'regor'
13
+ import {
14
+ type ComponentVariant,
15
+ resolveComponentClasses,
16
+ } from '../componentVariant'
17
+ import { defineFormCheckComponent } from './formCheck'
18
+
19
+ export interface AppForm {
20
+ action?: RefOrValue<string>
21
+ method?: RefOrValue<string>
22
+ tone?: RefOrValue<SemanticTone>
23
+ variant?: RefOrValue<ComponentVariant>
24
+ classes?: ComputedRef<string>
25
+ }
26
+
27
+ export interface FormMeta {}
28
+
29
+ export interface FormAssistLink {
30
+ href?: RefOrValue<string>
31
+ label?: RefOrValue<string>
32
+ target?: RefOrValue<string>
33
+ rel?: RefOrValue<string>
34
+ normalizedHref?: ComputedRef<string | undefined>
35
+ resolvedRel?: ComputedRef<string | undefined>
36
+ }
37
+
38
+ export interface FormSubmit {
39
+ label?: RefOrValue<string>
40
+ tone?: RefOrValue<SemanticTone>
41
+ variant?: RefOrValue<ComponentVariant>
42
+ }
43
+
44
+ export interface FormDivider {
45
+ label?: RefOrValue<string>
46
+ }
47
+
48
+ export interface FormStatus {
49
+ tone?: RefOrValue<SemanticTone>
50
+ variant?: RefOrValue<ComponentVariant>
51
+ hidden?: RefOrValue<boolean>
52
+ classes?: ComputedRef<string>
53
+ }
54
+
55
+ const appFormTemplate = html`<form
56
+ class="form-block"
57
+ :class="classes"
58
+ :action="action"
59
+ :method="method"
60
+ novalidate
61
+ >
62
+ <slot></slot>
63
+ </form>`
64
+
65
+ const formMetaTemplate = html`<div class="form-block__meta"><slot></slot></div>`
66
+
67
+ const formAssistLinkTemplate = html`<a
68
+ class="form-block__assist-link"
69
+ :href="normalizedHref"
70
+ r-if="normalizedHref"
71
+ :target="target"
72
+ :rel="resolvedRel"
73
+ >
74
+ {{ label }}
75
+ </a>`
76
+
77
+ const formSubmitTemplate = html`<Btn type="submit" :tone="tone" :variant="variant">{{ label }}</Btn>`
78
+
79
+ const formDividerTemplate = html`<div class="form-block__divider" :data-label="label"></div>`
80
+
81
+ const formStatusTemplate = html`<div
82
+ class="form-status"
83
+ :class="classes"
84
+ role="status"
85
+ aria-live="polite"
86
+ :hidden="hidden"
87
+ >
88
+ <slot></slot>
89
+ </div>`
90
+
91
+ function defineAppFormComponent() {
92
+ return defineComponent<AppForm>(appFormTemplate, {
93
+ props: ['action', 'method', 'tone', 'variant'],
94
+ context: (head) => resolveAppForm(head),
95
+ })
96
+ }
97
+
98
+ function defineFormMetaComponent() {
99
+ return defineComponent<FormMeta>(formMetaTemplate, {})
100
+ }
101
+
102
+ function defineFormAssistLinkComponent() {
103
+ return defineComponent<FormAssistLink>(formAssistLinkTemplate, {
104
+ props: ['href', 'label', 'target', 'rel'],
105
+ context: (head) => resolveFormAssistLink(head),
106
+ })
107
+ }
108
+
109
+ function defineFormSubmitComponent() {
110
+ return defineComponent<FormSubmit>(formSubmitTemplate, {
111
+ props: ['label', 'tone', 'variant'],
112
+ context: (head) => resolveFormSubmit(head),
113
+ })
114
+ }
115
+
116
+ function defineFormDividerComponent() {
117
+ return defineComponent<FormDivider>(formDividerTemplate, {
118
+ props: ['label'],
119
+ context: (head) => head.props,
120
+ })
121
+ }
122
+
123
+ function defineFormStatusComponent() {
124
+ return defineComponent<FormStatus>(formStatusTemplate, {
125
+ props: ['tone', 'variant', 'hidden'],
126
+ context: (head) => resolveFormStatus(head),
127
+ })
128
+ }
129
+
130
+ export function defineFormComponents() {
131
+ return {
132
+ appForm: defineAppFormComponent(),
133
+ formMeta: defineFormMetaComponent(),
134
+ formCheck: defineFormCheckComponent(),
135
+ formAssistLink: defineFormAssistLinkComponent(),
136
+ formSubmit: defineFormSubmitComponent(),
137
+ formDivider: defineFormDividerComponent(),
138
+ formStatus: defineFormStatusComponent(),
139
+ }
140
+ }
141
+
142
+ function resolveAppForm(head: ComponentHead<AppForm>): AppForm {
143
+ const props = head.props
144
+ return {
145
+ ...props,
146
+ action: resolvePublicAction(head),
147
+ classes: computed(() =>
148
+ resolveComponentClasses(props, {
149
+ defaultVariant: 'none',
150
+ }),
151
+ ),
152
+ }
153
+ }
154
+
155
+ function resolvePublicAction(head: ComponentHead<AppForm>) {
156
+ const action = unref(head.props.action)
157
+ if (!action) return action
158
+ return tryResolveTsSsgContext(head)?.resolvePublicHref(action) ?? action
159
+ }
160
+
161
+ function resolveFormAssistLink(
162
+ head: ComponentHead<FormAssistLink>,
163
+ ): FormAssistLink {
164
+ return {
165
+ ...head.props,
166
+ normalizedHref: computed(() =>
167
+ resolvePublicHref(
168
+ urlNormalizer.normalizeHref(unref(head.props.href)),
169
+ head,
170
+ ),
171
+ ),
172
+ resolvedRel: computed(
173
+ () =>
174
+ unref(head.props.rel) ||
175
+ (unref(head.props.target) === '_blank'
176
+ ? 'noopener noreferrer'
177
+ : undefined),
178
+ ),
179
+ }
180
+ }
181
+
182
+ function resolvePublicHref(
183
+ href: string | undefined,
184
+ head: ComponentHead<FormAssistLink>,
185
+ ) {
186
+ if (!href) return undefined
187
+ return tryResolveTsSsgContext(head)?.resolvePublicHref(href) ?? href
188
+ }
189
+
190
+ function resolveFormSubmit(head: ComponentHead<FormSubmit>): FormSubmit {
191
+ return {
192
+ ...head.props,
193
+ }
194
+ }
195
+
196
+ function resolveFormStatus(head: ComponentHead<FormStatus>): FormStatus {
197
+ return {
198
+ ...head.props,
199
+ classes: computed(() =>
200
+ resolveComponentClasses(head.props, {
201
+ defaultVariant: 'surface',
202
+ }),
203
+ ),
204
+ }
205
+ }
@@ -0,0 +1,96 @@
1
+ import type { SemanticTone } from '@purestack/ts-style'
2
+ import {
3
+ type ComponentHead,
4
+ type ComputedRef,
5
+ computed,
6
+ defineComponent,
7
+ html,
8
+ isRef,
9
+ type Ref,
10
+ type RefOrValue,
11
+ ref,
12
+ unref,
13
+ } from 'regor'
14
+ import { createAutoId } from '../autoId'
15
+ import { resolveComponentClasses } from '../componentVariant'
16
+
17
+ const resolveCheckId = createAutoId('form-check')
18
+
19
+ export class FormCheck {
20
+ readonly id: string
21
+ label?: RefOrValue<string>
22
+ name?: RefOrValue<string>
23
+ value?: RefOrValue<string>
24
+ checked?: Ref<boolean>
25
+ disabled?: RefOrValue<boolean>
26
+ tone?: RefOrValue<SemanticTone>
27
+ rootClass?: RefOrValue<string>
28
+ hasLabel?: ComputedRef<boolean>
29
+ classes?: ComputedRef<string>
30
+
31
+ constructor(props: FormCheck) {
32
+ Object.assign(this, props)
33
+ this.id = resolveCheckId(props.id)
34
+ if (!isRef(this.checked)) {
35
+ this.checked = ref<boolean>(resolveInitialChecked(this.checked))
36
+ }
37
+ }
38
+ }
39
+
40
+ const formCheckTemplate = html`<label class="form-block__check" :class="classes" :for="id">
41
+ <input
42
+ :id="id"
43
+ class="form-block__check-input"
44
+ type="checkbox"
45
+ :name="name"
46
+ :value="value"
47
+ :disabled="disabled"
48
+ r-model="checked"
49
+ r-inherit/>
50
+ <span class="form-block__check-control" aria-hidden="true"></span>
51
+ <span class="form-block__check-label" r-if="hasLabel">{{ label }}</span>
52
+ </label>`
53
+
54
+ export function defineFormCheckComponent() {
55
+ return defineComponent<FormCheck>(formCheckTemplate, {
56
+ props: [
57
+ 'id',
58
+ 'label',
59
+ 'name',
60
+ 'value',
61
+ 'checked',
62
+ 'disabled',
63
+ 'tone',
64
+ 'rootClass',
65
+ ],
66
+ context: (head) => resolveFormCheck(head),
67
+ })
68
+ }
69
+
70
+ function resolveFormCheck(head: ComponentHead<FormCheck>): FormCheck {
71
+ const check = new FormCheck(head.props)
72
+ check.hasLabel = computed(() => hasCheckLabel(head.props.label))
73
+ check.classes = computed(() =>
74
+ resolveComponentClasses(head.props, {
75
+ defaultVariant: 'none',
76
+ classes: [
77
+ check.hasLabel?.() ? undefined : 'form-block__check--bare',
78
+ head.props.rootClass,
79
+ ],
80
+ }),
81
+ )
82
+ return check
83
+ }
84
+
85
+ function hasCheckLabel(label: RefOrValue<string> | undefined) {
86
+ const value = unref(label)
87
+ return typeof value === 'string' ? value.trim().length > 0 : Boolean(value)
88
+ }
89
+
90
+ function resolveInitialChecked(value: unknown) {
91
+ if (typeof value === 'string') {
92
+ const normalized = value.trim().toLowerCase()
93
+ return normalized !== '' && normalized !== 'false'
94
+ }
95
+ return !!value
96
+ }
@@ -0,0 +1,184 @@
1
+ import type { SemanticTone } from '@purestack/ts-style'
2
+ import {
3
+ type ComponentHead,
4
+ type ComputedRef,
5
+ computed,
6
+ defineComponent,
7
+ html,
8
+ type Ref,
9
+ type RefOrValue,
10
+ ref,
11
+ unref,
12
+ } from 'regor'
13
+ import { createAutoId } from '../autoId'
14
+ import {
15
+ type ComponentVariant,
16
+ resolveComponentClasses,
17
+ } from '../componentVariant'
18
+
19
+ const resolveInputId = createAutoId('form-input')
20
+
21
+ export type FormInputFieldType =
22
+ | 'color'
23
+ | 'text'
24
+ | 'email'
25
+ | 'password'
26
+ | 'number'
27
+ | 'tel'
28
+ | 'url'
29
+ | 'search'
30
+ | 'date'
31
+ | 'time'
32
+ | 'datetime-local'
33
+ | 'month'
34
+ | 'week'
35
+
36
+ export class FormInputField {
37
+ readonly id: string
38
+ readonly label?: RefOrValue<string>
39
+ readonly tone?: RefOrValue<SemanticTone>
40
+ readonly variant?: RefOrValue<ComponentVariant>
41
+ declare readonly model: Ref<string | number>
42
+ readonly type?: RefOrValue<FormInputFieldType>
43
+ readonly name?: RefOrValue<string>
44
+ readonly required?: RefOrValue<boolean>
45
+ readonly autocomplete?: RefOrValue<string>
46
+ readonly min?: RefOrValue<number | string>
47
+ readonly step?: RefOrValue<number | string>
48
+ readonly placeholder?: RefOrValue<string>
49
+ readonly disabled?: RefOrValue<boolean>
50
+ readonly icon?: RefOrValue<string>
51
+ readonly iconEnd?: RefOrValue<string>
52
+ classes?: ComputedRef<string>
53
+
54
+ constructor(props: FormInputField) {
55
+ Object.assign(this, {
56
+ ...props,
57
+ })
58
+ this.id = resolveInputId(props.id)
59
+ if (!this.model) this.model = ref<string | number>('')
60
+ }
61
+
62
+ get isNumberField() {
63
+ return unref(this.type) === 'number'
64
+ }
65
+
66
+ increment = () => {
67
+ this.stepBy(1)
68
+ }
69
+
70
+ decrement = () => {
71
+ this.stepBy(-1)
72
+ }
73
+
74
+ private stepBy(direction: 1 | -1) {
75
+ const current = this.readCurrentNumber()
76
+ const step = this.readStep()
77
+ const min = this.readMin()
78
+ let next = current + direction * step
79
+ if (min !== undefined && next < min) {
80
+ next = min
81
+ }
82
+ const modelRef = this.model
83
+ if (modelRef) modelRef(next)
84
+ }
85
+
86
+ private readCurrentNumber() {
87
+ const raw = unref(this.model)
88
+ const numeric =
89
+ typeof raw === 'number' ? raw : Number.parseFloat(String(raw ?? '0'))
90
+ return Number.isFinite(numeric) ? numeric : 0
91
+ }
92
+
93
+ private readMin() {
94
+ const min = unref(this.min)
95
+ if (min === undefined) return undefined
96
+ const value = typeof min === 'number' ? min : Number.parseFloat(min)
97
+ return Number.isFinite(value) ? value : undefined
98
+ }
99
+
100
+ private readStep() {
101
+ const step = unref(this.step)
102
+ if (step === undefined) return 1
103
+ const value = typeof step === 'number' ? step : Number.parseFloat(step)
104
+ return Number.isFinite(value) && value > 0 ? value : 1
105
+ }
106
+ }
107
+
108
+ const formInputFieldTemplate = html`<label class="form-block__field" :for="id">
109
+ <span class="form-block__label" r-if="label">{{ label }}</span>
110
+ <div class="form-block__input-shell" :class="classes">
111
+ <Icon class="form-block__input-icon" :name="icon" r-if="icon"/>
112
+ <input
113
+ :id="id"
114
+ class="form-block__input"
115
+ :class="{ 'form-block__input--number': isNumberField }"
116
+ :type="isNumberField ? 'number' : type || 'text'"
117
+ :name="name"
118
+ :required="required"
119
+ :autocomplete="autocomplete || 'off'"
120
+ :min="min"
121
+ :step="step"
122
+ :placeholder="placeholder"
123
+ :disabled="disabled"
124
+ r-model="model"
125
+ r-inherit/>
126
+ <div class="form-block__number-controls" r-if="isNumberField">
127
+ <button
128
+ class="form-block__number-btn form-block__number-btn--up tone-fill-button-all"
129
+ type="button"
130
+ aria-label="Increase value"
131
+ @click="increment"
132
+ :disabled="disabled"
133
+ tabindex="-1"
134
+ >
135
+ +
136
+ </button>
137
+ <button
138
+ class="form-block__number-btn form-block__number-btn--down tone-fill-button-all"
139
+ type="button"
140
+ aria-label="Decrease value"
141
+ @click="decrement"
142
+ :disabled="disabled"
143
+ tabindex="-1"
144
+ >
145
+ -
146
+ </button>
147
+ </div>
148
+ <Icon class="form-block__input-icon" :name="iconEnd" r-if="iconEnd"/>
149
+ </div>
150
+ </label>`
151
+ export function defineFormInputField() {
152
+ return {
153
+ formInputField: defineComponent<FormInputField>(formInputFieldTemplate, {
154
+ props: [
155
+ 'id',
156
+ 'label',
157
+ 'tone',
158
+ 'variant',
159
+ 'model',
160
+ 'type',
161
+ 'name',
162
+ 'required',
163
+ 'autocomplete',
164
+ 'min',
165
+ 'step',
166
+ 'placeholder',
167
+ 'disabled',
168
+ 'icon',
169
+ 'iconEnd',
170
+ ],
171
+ context: (head) => resolveFormInputField(head),
172
+ }),
173
+ }
174
+ }
175
+
176
+ function resolveFormInputField(head: ComponentHead<FormInputField>) {
177
+ const field = new FormInputField(head.props)
178
+ field.classes = computed(() =>
179
+ resolveComponentClasses(head.props, {
180
+ defaultVariant: 'surfaceAlt',
181
+ }),
182
+ )
183
+ return field
184
+ }
@@ -0,0 +1,152 @@
1
+ import type { SemanticTone } from '@purestack/ts-style'
2
+ import {
3
+ type ComponentHead,
4
+ type ComputedRef,
5
+ computed,
6
+ defineComponent,
7
+ html,
8
+ type Ref,
9
+ type RefOrValue,
10
+ ref,
11
+ unref,
12
+ } from 'regor'
13
+ import { createAutoId } from '../autoId'
14
+ import {
15
+ type ComponentVariant,
16
+ resolveComponentClasses,
17
+ } from '../componentVariant'
18
+
19
+ const resolveSelectId = createAutoId('form-select')
20
+
21
+ export type FormSelectValue = string | number
22
+
23
+ export interface FormSelectOption {
24
+ label: RefOrValue<string>
25
+ value?: RefOrValue<FormSelectValue>
26
+ disabled?: RefOrValue<boolean>
27
+ }
28
+
29
+ export interface ResolvedFormSelectOption {
30
+ label: string
31
+ value: FormSelectValue
32
+ disabled?: boolean
33
+ }
34
+
35
+ export class FormSelectField {
36
+ readonly id: string
37
+ readonly label?: RefOrValue<string>
38
+ readonly tone?: RefOrValue<SemanticTone>
39
+ readonly variant?: RefOrValue<ComponentVariant>
40
+ declare readonly model: Ref<FormSelectValue>
41
+ readonly name?: RefOrValue<string>
42
+ readonly required?: RefOrValue<boolean>
43
+ readonly autocomplete?: RefOrValue<string>
44
+ readonly placeholder?: RefOrValue<string>
45
+ readonly disabled?: RefOrValue<boolean>
46
+ readonly icon?: RefOrValue<string>
47
+ readonly iconEnd?: RefOrValue<string>
48
+ readonly options?: RefOrValue<FormSelectOption[]>
49
+ classes?: ComputedRef<string>
50
+ resolvedOptions?: ComputedRef<ResolvedFormSelectOption[]>
51
+ resolvedIconEnd?: ComputedRef<string>
52
+ hasPlaceholder?: ComputedRef<boolean>
53
+
54
+ constructor(props: FormSelectField) {
55
+ Object.assign(this, {
56
+ ...props,
57
+ })
58
+ this.id = resolveSelectId(props.id)
59
+ if (!this.model) this.model = ref<FormSelectValue>('')
60
+ }
61
+ }
62
+
63
+ const formSelectFieldTemplate = html`<label class="form-block__field" :for="id">
64
+ <span class="form-block__label" r-if="label">{{ label }}</span>
65
+ <div
66
+ class="form-block__input-shell form-block__select-shell"
67
+ :class="classes"
68
+ >
69
+ <Icon
70
+ class="form-block__input-icon"
71
+ :name="icon"
72
+ r-if="icon"
73
+ style="pointer-events: none"/>
74
+ <select
75
+ :id="id"
76
+ class="form-block__input form-block__select"
77
+ :name="name"
78
+ :required="required"
79
+ :autocomplete="autocomplete || 'off'"
80
+ :disabled="disabled"
81
+ r-model="model"
82
+ r-inherit
83
+ >
84
+ <option value="" disabled r-if="hasPlaceholder">{{ placeholder }}</option>
85
+ <option
86
+ r-for="option in resolvedOptions"
87
+ :value="option.value"
88
+ :disabled="option.disabled"
89
+ >
90
+ {{ option.label }}
91
+ </option>
92
+ </select>
93
+ <Icon
94
+ class="form-block__input-icon"
95
+ :name="resolvedIconEnd"/>
96
+ </div>
97
+ </label>`
98
+
99
+ export function defineFormSelectField() {
100
+ return {
101
+ formSelectField: defineComponent<FormSelectField>(formSelectFieldTemplate, {
102
+ props: [
103
+ 'id',
104
+ 'label',
105
+ 'tone',
106
+ 'variant',
107
+ 'model',
108
+ 'name',
109
+ 'required',
110
+ 'autocomplete',
111
+ 'placeholder',
112
+ 'disabled',
113
+ 'icon',
114
+ 'iconEnd',
115
+ 'options',
116
+ ],
117
+ context: (head) => resolveFormSelectField(head),
118
+ }),
119
+ }
120
+ }
121
+
122
+ function resolveFormSelectField(head: ComponentHead<FormSelectField>) {
123
+ const field = new FormSelectField(head.props)
124
+ field.classes = computed(() =>
125
+ resolveComponentClasses(head.props, {
126
+ defaultVariant: 'surfaceAlt',
127
+ }),
128
+ )
129
+ field.resolvedOptions = computed(() => {
130
+ const options = (unref(head.props.options) || []) as Array<
131
+ RefOrValue<FormSelectOption>
132
+ >
133
+ return options.map((option) => resolveSelectOption(option))
134
+ })
135
+ field.resolvedIconEnd = computed(
136
+ () => unref(head.props.iconEnd) || 'lucide:chevron-down',
137
+ )
138
+ field.hasPlaceholder = computed(() => Boolean(unref(head.props.placeholder)))
139
+ return field
140
+ }
141
+
142
+ function resolveSelectOption(
143
+ optionInput: RefOrValue<FormSelectOption>,
144
+ ): ResolvedFormSelectOption {
145
+ const option = unref(optionInput)
146
+ const label = unref(option.label)
147
+ return {
148
+ label,
149
+ value: unref(option.value) ?? label,
150
+ disabled: unref(option.disabled),
151
+ }
152
+ }