@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.
- package/LICENSE +21 -0
- package/README.md +10 -0
- package/dist/ts-components.d.mts +1662 -0
- package/dist/ts-components.d.mts.map +1 -0
- package/dist/ts-components.mjs +9200 -0
- package/dist/ts-components.mjs.map +1 -0
- package/package.json +130 -0
- package/src/defineComponents.ts +87 -0
- package/src/index.ts +270 -0
- package/src/registerStyles.ts +63 -0
- package/src/standard/alertBox/alertBox.ts +64 -0
- package/src/standard/autoId.ts +25 -0
- package/src/standard/badge/badge.ts +41 -0
- package/src/standard/badge/badgeStyle.ts +21 -0
- package/src/standard/barChart/barChart.ts +338 -0
- package/src/standard/barChart/barChartStyle.ts +81 -0
- package/src/standard/btn/btn.ts +191 -0
- package/src/standard/btn/btnStyle.ts +59 -0
- package/src/standard/btnGroup/btnGroup.ts +208 -0
- package/src/standard/btnGroup/btnGroupStyle.ts +85 -0
- package/src/standard/chart/cartesianChart.ts +99 -0
- package/src/standard/chart/chartDefaults.ts +25 -0
- package/src/standard/chart/chartUtils.ts +84 -0
- package/src/standard/classicLogo/classicLogo.ts +429 -0
- package/src/standard/classicLogo/classicLogoStyle.ts +292 -0
- package/src/standard/componentVariant.ts +153 -0
- package/src/standard/composer/composer.ts +1035 -0
- package/src/standard/composer/composerCanvasInitialStyle.ts +225 -0
- package/src/standard/composer/composerCanvasRevertStyle.ts +14 -0
- package/src/standard/composer/composerCanvasSharedStyle.ts +60 -0
- package/src/standard/composer/composerCanvasStyleTypes.ts +8 -0
- package/src/standard/composer/composerCanvasThemeStyle.ts +53 -0
- package/src/standard/composer/composerStyle.ts +156 -0
- package/src/standard/composer/emailHtmlPolicy.ts +474 -0
- package/src/standard/consent/consent.ts +173 -0
- package/src/standard/consent/consentStyle.ts +44 -0
- package/src/standard/contactForm/contactForm.ts +135 -0
- package/src/standard/contactForm/contactFormStyle.ts +116 -0
- package/src/standard/doughnutChart/doughnutChart.ts +527 -0
- package/src/standard/doughnutChart/doughnutChartStyle.ts +64 -0
- package/src/standard/dropFiles/dropFiles.ts +480 -0
- package/src/standard/dropFiles/dropFilesStyle.ts +171 -0
- package/src/standard/expandablePanel/expandablePanel.ts +106 -0
- package/src/standard/expandablePanel/expandablePanelStyle.ts +56 -0
- package/src/standard/flex/flex.ts +269 -0
- package/src/standard/flex/flexStyle.ts +556 -0
- package/src/standard/footer/footer.ts +93 -0
- package/src/standard/footer/footerStyle.ts +39 -0
- package/src/standard/form/autoCompleteInput.ts +567 -0
- package/src/standard/form/form.ts +205 -0
- package/src/standard/form/formCheck.ts +96 -0
- package/src/standard/form/formInputField.ts +184 -0
- package/src/standard/form/formSelectField.ts +152 -0
- package/src/standard/form/formStyle.ts +585 -0
- package/src/standard/form/multiAutoCompleteInput.ts +880 -0
- package/src/standard/grid/grid.ts +177 -0
- package/src/standard/grid/gridStyle.ts +56 -0
- package/src/standard/icon/icon.ts +96 -0
- package/src/standard/icon/iconStyle.ts +50 -0
- package/src/standard/landing/codeShowcase.ts +54 -0
- package/src/standard/landing/comparisonColumn.ts +30 -0
- package/src/standard/landing/comparisonFeature.ts +22 -0
- package/src/standard/landing/comparisonTable.ts +38 -0
- package/src/standard/landing/ctaSection.ts +59 -0
- package/src/standard/landing/featureCard.ts +41 -0
- package/src/standard/landing/landing.ts +52 -0
- package/src/standard/landing/landingBand.ts +217 -0
- package/src/standard/landing/landingSection.ts +43 -0
- package/src/standard/landing/landingTypes.ts +141 -0
- package/src/standard/landing/metricItem.ts +25 -0
- package/src/standard/landing/metricStrip.ts +20 -0
- package/src/standard/lineChart/lineChart.ts +462 -0
- package/src/standard/lineChart/lineChartStyle.ts +91 -0
- package/src/standard/logo/logo.ts +192 -0
- package/src/standard/logo/logoStyle.ts +164 -0
- package/src/standard/modal/modal.ts +171 -0
- package/src/standard/modal/modalStore.ts +154 -0
- package/src/standard/modal/modalStyle.ts +190 -0
- package/src/standard/navMenu/navMenu.ts +278 -0
- package/src/standard/navMenu/navMenuStyle.ts +323 -0
- package/src/standard/pageLinks/pageLinks.ts +90 -0
- package/src/standard/pageScript/pageScript.ts +155 -0
- package/src/standard/pageToc/pageToc.ts +154 -0
- package/src/standard/pageToc/pageTocStyle.ts +372 -0
- package/src/standard/panel/panel.ts +55 -0
- package/src/standard/panel/panelStyle.ts +15 -0
- package/src/standard/pricing/pricing.ts +146 -0
- package/src/standard/searchBox/searchBox.ts +54 -0
- package/src/standard/searchBox/searchBoxStyle.ts +180 -0
- package/src/standard/sectionHeader/sectionHeader.ts +99 -0
- package/src/standard/signIn/signIn.ts +167 -0
- package/src/standard/signIn/signInStyle.ts +70 -0
- package/src/standard/tabs/tabs.ts +193 -0
- package/src/standard/tabs/tabsStyle.ts +231 -0
- package/src/standard/themeToggle/themeToggle.ts +33 -0
- package/src/standard/themeToggle/themeToggleStyle.ts +88 -0
- package/src/standard/toast/toastHost.ts +42 -0
- package/src/standard/toast/toastStore.ts +50 -0
- package/src/standard/topBar/topBar.ts +101 -0
- package/src/standard/topBar/topBarStyle.ts +169 -0
- package/src/standard/virtualList/variableVirtualTable.ts +371 -0
- package/src/standard/virtualList/virtualList.ts +423 -0
- package/src/standard/virtualList/virtualListStyle.ts +34 -0
- package/src/standard/virtualList/virtualTable.ts +179 -0
- package/src/standard/virtualList/virtualTableStyle.ts +24 -0
- 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
|
+
}
|