feat(13-03): port select + form-field components with CSS and helpers (GREEN)
- select_helpers.go: 9 helper functions verbatim from go-backend - select.templ: SelectProps/SelectOption structs, inline JS with __uiSelectInitAll and htmx:afterSwap re-init listener (Pitfall 6) - select.css: .ui-select-control (min-height 44px), .ui-select-menu (max-height 16rem) - form_field.templ: FormFieldProps with Label/For/Field/Error/Hint; conditional regions - form-field.css: .ui-form-field/.ui-form-label/.ui-form-hint/.ui-form-error - tailwind.input.css: add @import for select.css and form-field.css - All 6 TestSelect/TestFormField tests passing; full go test ./... is green
This commit is contained in:
parent
50e3fb0021
commit
52fb77d4f8
6 changed files with 559 additions and 0 deletions
22
backend/internal/web/ui/form-field.css
Normal file
22
backend/internal/web/ui/form-field.css
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
.ui-form-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-form-label {
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-form-hint {
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-form-error {
|
||||||
|
color: var(--color-status-danger-foreground);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
26
backend/internal/web/ui/form_field.templ
Normal file
26
backend/internal/web/ui/form_field.templ
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type FormFieldProps struct {
|
||||||
|
Label string
|
||||||
|
For string
|
||||||
|
Field templ.Component
|
||||||
|
Error string
|
||||||
|
Hint string
|
||||||
|
}
|
||||||
|
|
||||||
|
templ FormField(props FormFieldProps) {
|
||||||
|
<div class="ui-form-field">
|
||||||
|
if props.Label != "" {
|
||||||
|
<label for={ props.For } class="ui-form-label">{ props.Label }</label>
|
||||||
|
}
|
||||||
|
if props.Field != nil {
|
||||||
|
@props.Field
|
||||||
|
}
|
||||||
|
if props.Hint != "" {
|
||||||
|
<p class="ui-form-hint">{ props.Hint }</p>
|
||||||
|
}
|
||||||
|
if props.Error != "" {
|
||||||
|
<p class="ui-form-error">{ props.Error }</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
154
backend/internal/web/ui/select.css
Normal file
154
backend/internal/web/ui/select.css
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
.ui-select {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-native {
|
||||||
|
height: 0;
|
||||||
|
left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-control {
|
||||||
|
align-items: center;
|
||||||
|
appearance: none;
|
||||||
|
background: var(--color-surface-default);
|
||||||
|
border: 1px solid var(--color-border-default);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0.55rem 0.75rem 0.55rem 0.95rem;
|
||||||
|
text-align: left;
|
||||||
|
transition:
|
||||||
|
border-color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-control:focus-visible {
|
||||||
|
border-color: var(--color-brand-focus);
|
||||||
|
box-shadow: 0 0 0 3px var(--color-focus-ring-strong);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-control:disabled {
|
||||||
|
color: var(--color-text-faint);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-value-wrapper {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-placeholder {
|
||||||
|
color: var(--color-text-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-chip {
|
||||||
|
background: var(--color-surface-muted);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-arrow-zone {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-arrow-icon {
|
||||||
|
height: 1rem;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
width: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select.is-open .ui-select-arrow-icon {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-menu {
|
||||||
|
background: var(--color-surface-default);
|
||||||
|
border: 1px solid var(--color-border-default);
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
box-shadow: var(--shadow-surface-md);
|
||||||
|
display: grid;
|
||||||
|
gap: 0.25rem;
|
||||||
|
left: 0;
|
||||||
|
margin-top: 0.45rem;
|
||||||
|
max-height: 16rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.4rem;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 100%;
|
||||||
|
z-index: 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-menu[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-option {
|
||||||
|
align-items: center;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.7rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
font: inherit;
|
||||||
|
gap: 0.75rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.7rem 0.8rem;
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-option:hover,
|
||||||
|
.ui-select-option:focus-visible {
|
||||||
|
background: var(--color-surface-muted);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-option.is-selected {
|
||||||
|
background: var(--color-status-info-soft-bg);
|
||||||
|
color: var(--color-text-brand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-option:disabled {
|
||||||
|
color: var(--color-text-faint);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-option-text {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-option-check {
|
||||||
|
color: currentColor;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-select-option.is-selected .ui-select-option-check {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
251
backend/internal/web/ui/select.templ
Normal file
251
backend/internal/web/ui/select.templ
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
type SelectOption struct {
|
||||||
|
Value string
|
||||||
|
Label string
|
||||||
|
Disabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectProps struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
Placeholder string
|
||||||
|
Value string
|
||||||
|
Values []string
|
||||||
|
Multiple bool
|
||||||
|
Options []SelectOption
|
||||||
|
Attrs templ.Attributes
|
||||||
|
}
|
||||||
|
|
||||||
|
templ Select(props SelectProps) {
|
||||||
|
<div
|
||||||
|
class="ui-select"
|
||||||
|
data-ui-select-root
|
||||||
|
data-ui-select-multiple={ selectBoolData(props.Multiple) }
|
||||||
|
data-placeholder={ selectPlaceholder(props) }
|
||||||
|
data-selected-label={ selectSelectedLabel(props) }
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
id={ selectNativeID(props.ID, props.Name) }
|
||||||
|
name={ props.Name }
|
||||||
|
class="ui-select-native"
|
||||||
|
data-ui-select-native
|
||||||
|
if props.Multiple {
|
||||||
|
multiple
|
||||||
|
}
|
||||||
|
{ props.Attrs... }
|
||||||
|
>
|
||||||
|
if !props.Multiple && selectPlaceholder(props) != "" {
|
||||||
|
<option value="">{ selectPlaceholder(props) }</option>
|
||||||
|
}
|
||||||
|
for _, option := range props.Options {
|
||||||
|
<option
|
||||||
|
value={ option.Value }
|
||||||
|
if option.Disabled {
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
if selectOptionSelected(props, option.Value) {
|
||||||
|
selected
|
||||||
|
}
|
||||||
|
>{ option.Label }</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
id={ inputID(props.ID, props.Name) }
|
||||||
|
type="button"
|
||||||
|
class="ui-select-control"
|
||||||
|
data-ui-select-trigger
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls={ selectMenuID(props.ID, props.Name) }
|
||||||
|
if selectIsDisabled(props.Attrs) {
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="ui-select-value-wrapper" data-ui-select-label>
|
||||||
|
if props.Multiple {
|
||||||
|
if len(selectSelectedLabels(props)) == 0 {
|
||||||
|
<span class="ui-select-placeholder">{ selectPlaceholder(props) }</span>
|
||||||
|
} else {
|
||||||
|
for _, label := range selectSelectedLabels(props) {
|
||||||
|
<span class="ui-select-chip">{ label }</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if selectSelectedLabel(props) != "" {
|
||||||
|
{ selectSelectedLabel(props) }
|
||||||
|
} else {
|
||||||
|
<span class="ui-select-placeholder">{ selectPlaceholder(props) }</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="ui-select-arrow-zone">
|
||||||
|
<svg class="ui-select-arrow-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="m6 9 6 6 6-6"></path>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
id={ selectMenuID(props.ID, props.Name) }
|
||||||
|
class="ui-select-menu"
|
||||||
|
data-ui-select-menu
|
||||||
|
role="listbox"
|
||||||
|
if props.Multiple {
|
||||||
|
aria-multiselectable="true"
|
||||||
|
}
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
for _, option := range props.Options {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={ selectMenuOptionClass(selectOptionSelected(props, option.Value), option.Disabled) }
|
||||||
|
data-ui-select-option
|
||||||
|
data-value={ option.Value }
|
||||||
|
data-label={ option.Label }
|
||||||
|
role="option"
|
||||||
|
aria-selected={ selectBoolData(selectOptionSelected(props, option.Value)) }
|
||||||
|
if option.Disabled {
|
||||||
|
disabled
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span class="ui-select-option-text">{ option.Label }</span>
|
||||||
|
<span class="ui-select-option-check" aria-hidden="true">✓</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
if (!window.__uiSelectInitAll) {
|
||||||
|
window.__uiSelectSetOpen = function (root, open) {
|
||||||
|
var trigger = root.querySelector("[data-ui-select-trigger]");
|
||||||
|
var menu = root.querySelector("[data-ui-select-menu]");
|
||||||
|
if (!trigger || !menu) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.classList.toggle("is-open", open);
|
||||||
|
trigger.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
|
menu.hidden = !open;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.__uiSelectCloseAll = function (exceptRoot) {
|
||||||
|
document.querySelectorAll("[data-ui-select-root].is-open").forEach(function (root) {
|
||||||
|
if (root !== exceptRoot) {
|
||||||
|
window.__uiSelectSetOpen(root, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.__uiSelectSync = function (root) {
|
||||||
|
var nativeSelect = root.querySelector("[data-ui-select-native]");
|
||||||
|
var outlet = root.querySelector("[data-ui-select-label]");
|
||||||
|
var placeholder = root.getAttribute("data-placeholder") || "";
|
||||||
|
var multiple = root.getAttribute("data-ui-select-multiple") === "true";
|
||||||
|
if (!nativeSelect || !outlet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var labels = Array.from(nativeSelect.options).filter(function (option) {
|
||||||
|
return option.selected && option.value !== "";
|
||||||
|
}).map(function (option) {
|
||||||
|
return option.textContent;
|
||||||
|
});
|
||||||
|
|
||||||
|
root.setAttribute("data-selected-label", labels.join(", "));
|
||||||
|
outlet.innerHTML = "";
|
||||||
|
|
||||||
|
if (labels.length === 0) {
|
||||||
|
var placeholderNode = document.createElement("span");
|
||||||
|
placeholderNode.className = "ui-select-placeholder";
|
||||||
|
placeholderNode.textContent = placeholder;
|
||||||
|
outlet.appendChild(placeholderNode);
|
||||||
|
} else if (multiple) {
|
||||||
|
labels.forEach(function (label) {
|
||||||
|
var chip = document.createElement("span");
|
||||||
|
chip.className = "ui-select-chip";
|
||||||
|
chip.textContent = label;
|
||||||
|
outlet.appendChild(chip);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
outlet.textContent = labels[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
root.querySelectorAll("[data-ui-select-option]").forEach(function (optionButton) {
|
||||||
|
var selected = Array.from(nativeSelect.options).some(function (option) {
|
||||||
|
return option.value === optionButton.getAttribute("data-value") && option.selected;
|
||||||
|
});
|
||||||
|
optionButton.classList.toggle("is-selected", selected);
|
||||||
|
optionButton.setAttribute("aria-selected", selected ? "true" : "false");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.__uiSelectToggleValue = function (root, optionButton) {
|
||||||
|
var nativeSelect = root.querySelector("[data-ui-select-native]");
|
||||||
|
var multiple = root.getAttribute("data-ui-select-multiple") === "true";
|
||||||
|
var value = optionButton.getAttribute("data-value");
|
||||||
|
if (!nativeSelect || value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.from(nativeSelect.options).forEach(function (option) {
|
||||||
|
if (option.value !== value && !multiple) {
|
||||||
|
option.selected = false;
|
||||||
|
}
|
||||||
|
if (option.value === value) {
|
||||||
|
option.selected = multiple ? !option.selected : true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.__uiSelectSync(root);
|
||||||
|
nativeSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||||
|
|
||||||
|
if (!multiple) {
|
||||||
|
window.__uiSelectSetOpen(root, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.__uiSelectInitAll = function (scope) {
|
||||||
|
(scope || document).querySelectorAll("[data-ui-select-root]").forEach(function (root) {
|
||||||
|
window.__uiSelectSync(root);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("click", function (event) {
|
||||||
|
var optionButton = event.target.closest("[data-ui-select-option]");
|
||||||
|
if (optionButton) {
|
||||||
|
var optionRoot = optionButton.closest("[data-ui-select-root]");
|
||||||
|
if (optionRoot) {
|
||||||
|
window.__uiSelectToggleValue(optionRoot, optionButton);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trigger = event.target.closest("[data-ui-select-trigger]");
|
||||||
|
if (trigger) {
|
||||||
|
var root = trigger.closest("[data-ui-select-root]");
|
||||||
|
var shouldOpen = root && !root.classList.contains("is-open");
|
||||||
|
window.__uiSelectCloseAll(root);
|
||||||
|
if (root) {
|
||||||
|
window.__uiSelectSetOpen(root, shouldOpen);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!event.target.closest("[data-ui-select-root]")) {
|
||||||
|
window.__uiSelectCloseAll(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function (event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
window.__uiSelectCloseAll(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("htmx:afterSwap", function (event) {
|
||||||
|
window.__uiSelectInitAll(event.target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.__uiSelectInitAll(document);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
104
backend/internal/web/ui/select_helpers.go
Normal file
104
backend/internal/web/ui/select_helpers.go
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
)
|
||||||
|
|
||||||
|
func selectPlaceholder(props SelectProps) string {
|
||||||
|
if props.Placeholder != "" {
|
||||||
|
return props.Placeholder
|
||||||
|
}
|
||||||
|
if props.Multiple {
|
||||||
|
return "Select values"
|
||||||
|
}
|
||||||
|
return "Select an option"
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectNativeID(id string, name string) string {
|
||||||
|
baseID := inputID(id, name)
|
||||||
|
if baseID == "" {
|
||||||
|
return "ui-select-native"
|
||||||
|
}
|
||||||
|
return baseID + "-native"
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectMenuID(id string, name string) string {
|
||||||
|
baseID := inputID(id, name)
|
||||||
|
if baseID == "" {
|
||||||
|
return "ui-select-menu"
|
||||||
|
}
|
||||||
|
return baseID + "-menu"
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectBoolData(value bool) string {
|
||||||
|
if value {
|
||||||
|
return "true"
|
||||||
|
}
|
||||||
|
return "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectSelectedValues(props SelectProps) []string {
|
||||||
|
if props.Multiple {
|
||||||
|
return props.Values
|
||||||
|
}
|
||||||
|
if props.Value == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []string{props.Value}
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectOptionSelected(props SelectProps, value string) bool {
|
||||||
|
for _, selected := range selectSelectedValues(props) {
|
||||||
|
if selected == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectSelectedLabels(props SelectProps) []string {
|
||||||
|
var labels []string
|
||||||
|
for _, option := range props.Options {
|
||||||
|
if selectOptionSelected(props, option.Value) {
|
||||||
|
labels = append(labels, option.Label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectSelectedLabel(props SelectProps) string {
|
||||||
|
return strings.Join(selectSelectedLabels(props), ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectMenuOptionClass(selected bool, disabled bool) string {
|
||||||
|
className := "ui-select-option"
|
||||||
|
if selected {
|
||||||
|
className += " is-selected"
|
||||||
|
}
|
||||||
|
if disabled {
|
||||||
|
className += " is-disabled"
|
||||||
|
}
|
||||||
|
return className
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectIsDisabled(attrs templ.Attributes) bool {
|
||||||
|
if attrs == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok := attrs["disabled"]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case bool:
|
||||||
|
return typed
|
||||||
|
case string:
|
||||||
|
return typed != "" && typed != "false"
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,3 +11,5 @@
|
||||||
@import "./internal/web/ui/card.css";
|
@import "./internal/web/ui/card.css";
|
||||||
@import "./internal/web/ui/input.css";
|
@import "./internal/web/ui/input.css";
|
||||||
@import "./internal/web/ui/textarea.css";
|
@import "./internal/web/ui/textarea.css";
|
||||||
|
@import "./internal/web/ui/select.css";
|
||||||
|
@import "./internal/web/ui/form-field.css";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue