Basic Form Components
parent
3495e9d544
commit
1e702a0f58
@ -1,3 +1,5 @@
|
|||||||
|
import type { CFormControlSize } from '../form/types'
|
||||||
|
|
||||||
export type CButtonVariant = 'default' | 'primary' | 'success' | 'warning' | 'danger'
|
export type CButtonVariant = 'default' | 'primary' | 'success' | 'warning' | 'danger'
|
||||||
export type CButtonType = 'button' | 'submit' | 'reset'
|
export type CButtonType = 'button' | 'submit' | 'reset'
|
||||||
export type CButtonSize = 'small' | 'medium' | 'large'
|
export type CButtonSize = CFormControlSize
|
||||||
|
|||||||
@ -0,0 +1,217 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watchEffect } from 'vue'
|
||||||
|
|
||||||
|
import type { CFormControlSize } from '../form/types'
|
||||||
|
import { useFormControl } from '../form/useFormControl'
|
||||||
|
import type { CCheckboxModelValue, CCheckboxValue } from './types'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: CCheckboxModelValue
|
||||||
|
value?: CCheckboxValue
|
||||||
|
trueValue?: CCheckboxValue
|
||||||
|
falseValue?: CCheckboxValue
|
||||||
|
size?: CFormControlSize
|
||||||
|
indeterminate?: boolean
|
||||||
|
threeState?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
required?: boolean
|
||||||
|
invalid?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: false,
|
||||||
|
value: true,
|
||||||
|
trueValue: true,
|
||||||
|
falseValue: false,
|
||||||
|
size: 'medium',
|
||||||
|
indeterminate: false,
|
||||||
|
threeState: false,
|
||||||
|
disabled: false,
|
||||||
|
required: false,
|
||||||
|
invalid: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: CCheckboxModelValue]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const input = ref<HTMLInputElement>()
|
||||||
|
const { controlId, describedBy, invalid, required } = useFormControl(props)
|
||||||
|
const checked = computed(() =>
|
||||||
|
Array.isArray(props.modelValue)
|
||||||
|
? props.modelValue.some((entry) => Object.is(entry, props.value))
|
||||||
|
: Object.is(props.modelValue, props.trueValue),
|
||||||
|
)
|
||||||
|
const usesThreeStates = computed(() => props.threeState && !Array.isArray(props.modelValue))
|
||||||
|
const isIndeterminate = computed(() =>
|
||||||
|
usesThreeStates.value ? props.modelValue === null : props.indeterminate,
|
||||||
|
)
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (input.value) input.value.indeterminate = isIndeterminate.value
|
||||||
|
})
|
||||||
|
|
||||||
|
function updateValue(event: Event) {
|
||||||
|
const nextChecked = (event.target as HTMLInputElement).checked
|
||||||
|
|
||||||
|
if (usesThreeStates.value) {
|
||||||
|
if (Object.is(props.modelValue, props.falseValue)) {
|
||||||
|
emit('update:modelValue', props.trueValue)
|
||||||
|
} else if (Object.is(props.modelValue, props.trueValue)) {
|
||||||
|
emit('update:modelValue', null)
|
||||||
|
} else {
|
||||||
|
emit('update:modelValue', props.falseValue)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(props.modelValue)) {
|
||||||
|
const next = props.modelValue.filter((entry) => !Object.is(entry, props.value))
|
||||||
|
if (nextChecked) next.push(props.value)
|
||||||
|
emit('update:modelValue', next)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('update:modelValue', nextChecked ? props.trueValue : props.falseValue)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label class="c-checkbox" :class="[`is-${size}`, { 'is-disabled': disabled }]">
|
||||||
|
<input
|
||||||
|
ref="input"
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="input"
|
||||||
|
type="checkbox"
|
||||||
|
:id="controlId"
|
||||||
|
:checked="checked"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="required"
|
||||||
|
:aria-checked="isIndeterminate ? 'mixed' : undefined"
|
||||||
|
:aria-invalid="invalid ? 'true' : undefined"
|
||||||
|
:aria-describedby="describedBy"
|
||||||
|
@change="updateValue"
|
||||||
|
/>
|
||||||
|
<span v-if="$slots.default" class="label"><slot /></span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-checkbox {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: fit-content;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--c-text-color, #20242a);
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.35;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
flex: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 1px 0 0;
|
||||||
|
appearance: none;
|
||||||
|
cursor: inherit;
|
||||||
|
background: var(--c-input-background, #fff);
|
||||||
|
border: 1px solid var(--c-control-border-color, #bfc5ce);
|
||||||
|
border-radius: 2px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked,
|
||||||
|
&:indeterminate {
|
||||||
|
background: var(--c-primary-color, #286aa6);
|
||||||
|
border-color: var(--c-primary-border-color, #245f95);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked::after {
|
||||||
|
top: 1px;
|
||||||
|
left: 4px;
|
||||||
|
display: block;
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
border: solid #fff;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:indeterminate::after {
|
||||||
|
top: 6px;
|
||||||
|
left: 3px;
|
||||||
|
display: block;
|
||||||
|
width: 8px;
|
||||||
|
height: 2px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--c-focus-color, #3578c6);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-invalid='true'] {
|
||||||
|
border-color: var(--c-danger-color, #b42318);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
color: var(--c-disabled-text-color, #8a9099);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
background-color: var(--c-disabled-background-color, #f1f3f5);
|
||||||
|
border-color: var(--c-disabled-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-small {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
|
||||||
|
&:checked::after {
|
||||||
|
top: 0;
|
||||||
|
left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:indeterminate::after {
|
||||||
|
top: 5px;
|
||||||
|
left: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-large {
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
|
||||||
|
&:checked::after {
|
||||||
|
top: 2px;
|
||||||
|
left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:indeterminate::after {
|
||||||
|
top: 7px;
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
export type CCheckboxValue = string | number | boolean
|
||||||
|
export type CCheckboxModelValue = CCheckboxValue | null | CCheckboxValue[]
|
||||||
@ -0,0 +1,112 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, provide, useId, useSlots } from 'vue'
|
||||||
|
|
||||||
|
import { cFormFieldKey } from './context'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
label?: string
|
||||||
|
for?: string
|
||||||
|
hint?: string
|
||||||
|
error?: string
|
||||||
|
required?: boolean
|
||||||
|
invalid?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
label: undefined,
|
||||||
|
for: undefined,
|
||||||
|
hint: undefined,
|
||||||
|
error: undefined,
|
||||||
|
required: false,
|
||||||
|
invalid: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
const uid = useId()
|
||||||
|
const controlId = computed(() => props.for ?? `c-field-${uid}`)
|
||||||
|
const hasHint = computed(() => Boolean(props.hint || slots.hint))
|
||||||
|
const hasError = computed(() => Boolean(props.error || slots.error))
|
||||||
|
const invalid = computed(() => props.invalid || hasError.value)
|
||||||
|
const hintId = computed(() => `${controlId.value}-hint`)
|
||||||
|
const errorId = computed(() => `${controlId.value}-error`)
|
||||||
|
const describedBy = computed(() => {
|
||||||
|
const ids = []
|
||||||
|
if (hasHint.value) ids.push(hintId.value)
|
||||||
|
if (hasError.value) ids.push(errorId.value)
|
||||||
|
return ids.length ? ids.join(' ') : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
provide(cFormFieldKey, {
|
||||||
|
controlId,
|
||||||
|
describedBy,
|
||||||
|
invalid,
|
||||||
|
required: computed(() => props.required),
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="c-form-field" :class="{ 'is-invalid': invalid }">
|
||||||
|
<label v-if="label || $slots.label" class="label" :for="controlId">
|
||||||
|
<slot name="label">{{ label }}</slot>
|
||||||
|
<span v-if="required" class="required" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="control">
|
||||||
|
<slot
|
||||||
|
:id="controlId"
|
||||||
|
:described-by="describedBy"
|
||||||
|
:invalid="invalid"
|
||||||
|
:required="required"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasHint" :id="hintId" class="hint">
|
||||||
|
<slot name="hint">{{ hint }}</slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="hasError" :id="errorId" class="error" role="alert">
|
||||||
|
<slot name="error">{{ error }}</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-form-field {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 4px;
|
||||||
|
color: var(--c-text-color, #20242a);
|
||||||
|
font-family: var(--c-font-family, system-ui, -apple-system, "Segoe UI", sans-serif);
|
||||||
|
font-size: var(--c-font-size, 13px);
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: fit-content;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required {
|
||||||
|
margin-inline-start: 3px;
|
||||||
|
color: var(--c-danger-color, #b42318);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint,
|
||||||
|
.error {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--c-danger-color, #b42318);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import type { ComputedRef, InjectionKey } from 'vue'
|
||||||
|
|
||||||
|
export interface CFormFieldContext {
|
||||||
|
controlId: ComputedRef<string>
|
||||||
|
describedBy: ComputedRef<string | undefined>
|
||||||
|
invalid: ComputedRef<boolean>
|
||||||
|
required: ComputedRef<boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cFormFieldKey: InjectionKey<CFormFieldContext> = Symbol('c-form-field')
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type CFormControlSize = 'small' | 'medium' | 'large'
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { computed, inject, useAttrs } from 'vue'
|
||||||
|
|
||||||
|
import { cFormFieldKey } from '../form-field/context'
|
||||||
|
|
||||||
|
export function useFormControl(props: { invalid: boolean; required: boolean }) {
|
||||||
|
const attrs = useAttrs()
|
||||||
|
const field = inject(cFormFieldKey, undefined)
|
||||||
|
|
||||||
|
const controlId = computed(() => {
|
||||||
|
const id = attrs.id
|
||||||
|
return typeof id === 'string' ? id : field?.controlId.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const describedBy = computed(() => {
|
||||||
|
const value = attrs['aria-describedby']
|
||||||
|
return typeof value === 'string' ? value : field?.describedBy.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const invalid = computed(() => props.invalid || Boolean(field?.invalid.value))
|
||||||
|
const required = computed(() => props.required || Boolean(field?.required.value))
|
||||||
|
|
||||||
|
return { controlId, describedBy, invalid, required }
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { CFormControlSize } from '../form/types'
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
tag?: string
|
||||||
|
size?: CFormControlSize
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
tag: 'span',
|
||||||
|
size: 'medium',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="tag" class="c-input-addon" :class="`is-${size}`">
|
||||||
|
<slot />
|
||||||
|
</component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-input-addon {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
flex: none;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 30px;
|
||||||
|
min-width: 30px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: var(--c-subtle-surface-color, #f7f8fa);
|
||||||
|
border: 1px solid var(--c-control-border-color, #bfc5ce);
|
||||||
|
border-radius: var(--c-border-radius, 3px);
|
||||||
|
|
||||||
|
&.is-small {
|
||||||
|
height: 26px;
|
||||||
|
min-width: 26px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-large {
|
||||||
|
height: 34px;
|
||||||
|
min-width: 34px;
|
||||||
|
padding: 5px 9px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="c-input-group">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-input-group {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
> :deep(*) {
|
||||||
|
margin-inline-start: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :deep(:first-child) {
|
||||||
|
margin-inline-start: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :deep(.c-input),
|
||||||
|
> :deep(.c-password),
|
||||||
|
> :deep(.c-number-input) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :deep(.c-input),
|
||||||
|
> :deep(.c-button),
|
||||||
|
> :deep(.c-input-addon) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :deep(:first-child.c-input),
|
||||||
|
> :deep(:first-child.c-button),
|
||||||
|
> :deep(:first-child.c-input-addon) {
|
||||||
|
border-start-start-radius: var(--c-border-radius, 3px);
|
||||||
|
border-end-start-radius: var(--c-border-radius, 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
> :deep(:last-child.c-input),
|
||||||
|
> :deep(:last-child.c-button),
|
||||||
|
> :deep(:last-child.c-input-addon) {
|
||||||
|
border-start-end-radius: var(--c-border-radius, 3px);
|
||||||
|
border-end-end-radius: var(--c-border-radius, 3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
> :deep(.c-password:not(:first-child) .input),
|
||||||
|
> :deep(.c-number-input:not(:first-child) .decrement),
|
||||||
|
> :deep(.c-number-input:not(:first-child) .input:first-child) {
|
||||||
|
border-start-start-radius: 0;
|
||||||
|
border-end-start-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :deep(.c-password:not(:last-child) .toggle),
|
||||||
|
> :deep(.c-password:not(:last-child) .input:last-child),
|
||||||
|
> :deep(.c-number-input:not(:last-child) .increment),
|
||||||
|
> :deep(.c-number-input:not(:last-child) .input:last-child) {
|
||||||
|
border-start-end-radius: 0;
|
||||||
|
border-end-end-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
> :deep(:focus-within) {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { InputHTMLAttributes } from 'vue'
|
||||||
|
|
||||||
|
import { useFormControl } from '../form/useFormControl'
|
||||||
|
import type { CFormControlSize } from '../form/types'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: string | null
|
||||||
|
type?: InputHTMLAttributes['type']
|
||||||
|
size?: CFormControlSize
|
||||||
|
disabled?: boolean
|
||||||
|
readonly?: boolean
|
||||||
|
required?: boolean
|
||||||
|
invalid?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: '',
|
||||||
|
type: 'text',
|
||||||
|
size: 'medium',
|
||||||
|
disabled: false,
|
||||||
|
readonly: false,
|
||||||
|
required: false,
|
||||||
|
invalid: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { controlId, describedBy, invalid, required } = useFormControl(props)
|
||||||
|
|
||||||
|
function updateValue(event: Event) {
|
||||||
|
emit('update:modelValue', (event.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="c-input"
|
||||||
|
:class="[`is-${size}`, { 'is-invalid': invalid }]"
|
||||||
|
:id="controlId"
|
||||||
|
:value="modelValue ?? ''"
|
||||||
|
:type="type"
|
||||||
|
:disabled="disabled"
|
||||||
|
:readonly="readonly"
|
||||||
|
:required="required"
|
||||||
|
:aria-invalid="invalid ? 'true' : undefined"
|
||||||
|
:aria-describedby="describedBy"
|
||||||
|
@input="updateValue"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 4px 7px;
|
||||||
|
color: var(--c-text-color, #20242a);
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.2;
|
||||||
|
background: var(--c-input-background, #fff);
|
||||||
|
border: 1px solid var(--c-control-border-color, #bfc5ce);
|
||||||
|
border-radius: var(--c-border-radius, 3px);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--c-placeholder-color, #7a828d);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:read-only) {
|
||||||
|
border-color: var(--c-control-hover-border-color, #929aa6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
border-color: var(--c-focus-color, #3578c6);
|
||||||
|
outline: 1px solid var(--c-focus-color, #3578c6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--c-disabled-text-color, #8a9099);
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: var(--c-disabled-background-color, #f1f3f5);
|
||||||
|
border-color: var(--c-disabled-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:read-only:not(:disabled) {
|
||||||
|
background: var(--c-readonly-background-color, #f7f8fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-invalid {
|
||||||
|
border-color: var(--c-danger-color, #b42318);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline-color: var(--c-danger-color, #b42318);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-small {
|
||||||
|
height: 26px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-large {
|
||||||
|
height: 34px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import CButton from '../button/CButton.vue'
|
||||||
|
import { useFormControl } from '../form/useFormControl'
|
||||||
|
import type { CFormControlSize } from '../form/types'
|
||||||
|
import CInput from '../input/CInput.vue'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: string | null
|
||||||
|
size?: CFormControlSize
|
||||||
|
disabled?: boolean
|
||||||
|
readonly?: boolean
|
||||||
|
required?: boolean
|
||||||
|
invalid?: boolean
|
||||||
|
revealable?: boolean
|
||||||
|
visible?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: '',
|
||||||
|
size: 'medium',
|
||||||
|
disabled: false,
|
||||||
|
readonly: false,
|
||||||
|
required: false,
|
||||||
|
invalid: false,
|
||||||
|
revealable: true,
|
||||||
|
visible: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
'update:visible': [visible: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const internalVisible = ref(false)
|
||||||
|
const isVisible = computed(() => props.visible ?? internalVisible.value)
|
||||||
|
const { controlId, describedBy, invalid, required } = useFormControl(props)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(visible) => {
|
||||||
|
if (visible !== undefined) internalVisible.value = visible
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function toggleVisibility() {
|
||||||
|
const visible = !isVisible.value
|
||||||
|
internalVisible.value = visible
|
||||||
|
emit('update:visible', visible)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="c-password" :class="[`is-${size}`, { 'is-invalid': invalid }]">
|
||||||
|
<CInput
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="input"
|
||||||
|
:id="controlId"
|
||||||
|
:model-value="modelValue"
|
||||||
|
:type="isVisible ? 'text' : 'password'"
|
||||||
|
:size="size"
|
||||||
|
:disabled="disabled"
|
||||||
|
:readonly="readonly"
|
||||||
|
:required="required"
|
||||||
|
:invalid="invalid"
|
||||||
|
:aria-describedby="describedBy"
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CButton
|
||||||
|
v-if="revealable"
|
||||||
|
class="toggle"
|
||||||
|
:size="size"
|
||||||
|
:icon="isVisible ? '⊘' : '◉'"
|
||||||
|
:disabled="disabled"
|
||||||
|
:aria-controls="controlId"
|
||||||
|
:aria-pressed="isVisible"
|
||||||
|
:aria-label="isVisible ? 'Hide password' : 'Show password'"
|
||||||
|
@click="toggleVisibility"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-password {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
border-start-end-radius: 0;
|
||||||
|
border-end-end-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
flex: none;
|
||||||
|
margin-inline-start: -1px;
|
||||||
|
border-start-start-radius: 0;
|
||||||
|
border-end-start-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import type { CFormControlSize } from '../form/types'
|
||||||
|
import { useFormControl } from '../form/useFormControl'
|
||||||
|
import type { CRadioValue } from './types'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: CRadioValue | null
|
||||||
|
value: CRadioValue
|
||||||
|
size?: CFormControlSize
|
||||||
|
disabled?: boolean
|
||||||
|
required?: boolean
|
||||||
|
invalid?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: null,
|
||||||
|
size: 'medium',
|
||||||
|
disabled: false,
|
||||||
|
required: false,
|
||||||
|
invalid: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: CRadioValue]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { controlId, describedBy, invalid, required } = useFormControl(props)
|
||||||
|
const checked = computed(() => Object.is(props.modelValue, props.value))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<label class="c-radio" :class="[`is-${size}`, { 'is-disabled': disabled }]">
|
||||||
|
<input
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="input"
|
||||||
|
type="radio"
|
||||||
|
:id="controlId"
|
||||||
|
:value="value"
|
||||||
|
:checked="checked"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="required"
|
||||||
|
:aria-invalid="invalid ? 'true' : undefined"
|
||||||
|
:aria-describedby="describedBy"
|
||||||
|
@change="emit('update:modelValue', value)"
|
||||||
|
/>
|
||||||
|
<span v-if="$slots.default" class="label"><slot /></span>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-radio {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: fit-content;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--c-text-color, #20242a);
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.35;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
flex: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin: 1px 0 0;
|
||||||
|
appearance: none;
|
||||||
|
cursor: inherit;
|
||||||
|
background: var(--c-input-background, #fff);
|
||||||
|
border: 1px solid var(--c-control-border-color, #bfc5ce);
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
display: none;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
content: '';
|
||||||
|
background: var(--c-primary-color, #286aa6);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:checked {
|
||||||
|
border-color: var(--c-primary-color, #286aa6);
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--c-focus-color, #3578c6);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-invalid='true'] {
|
||||||
|
border-color: var(--c-danger-color, #b42318);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-disabled {
|
||||||
|
color: var(--c-disabled-text-color, #8a9099);
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
background: var(--c-disabled-background-color, #f1f3f5);
|
||||||
|
border-color: var(--c-disabled-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-small {
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-large {
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export type CRadioValue = string | number | boolean
|
||||||
@ -0,0 +1,217 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import type { CFormControlSize } from '../form/types'
|
||||||
|
import { useFormControl } from '../form/useFormControl'
|
||||||
|
import type {
|
||||||
|
CSelectKeyAccessor,
|
||||||
|
CSelectLabelAccessor,
|
||||||
|
CSelectOption,
|
||||||
|
CSelectValue,
|
||||||
|
CSelectValueAccessor,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: CSelectValue | null
|
||||||
|
options?: object[]
|
||||||
|
optionLabel?: CSelectLabelAccessor
|
||||||
|
optionValue?: CSelectValueAccessor
|
||||||
|
optionKey?: CSelectKeyAccessor
|
||||||
|
placeholder?: string
|
||||||
|
size?: CFormControlSize
|
||||||
|
disabled?: boolean
|
||||||
|
required?: boolean
|
||||||
|
invalid?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: null,
|
||||||
|
options: () => [],
|
||||||
|
optionLabel: undefined,
|
||||||
|
optionValue: undefined,
|
||||||
|
optionKey: undefined,
|
||||||
|
placeholder: undefined,
|
||||||
|
size: 'medium',
|
||||||
|
disabled: false,
|
||||||
|
required: false,
|
||||||
|
invalid: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: CSelectValue | null]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { controlId, describedBy, invalid, required } = useFormControl(props)
|
||||||
|
const objectKeys = new WeakMap<object, number>()
|
||||||
|
let nextObjectKey = 0
|
||||||
|
|
||||||
|
const selection = computed({
|
||||||
|
get: () => props.modelValue ?? null,
|
||||||
|
set: (value: CSelectValue | null) => emit('update:modelValue', value),
|
||||||
|
})
|
||||||
|
|
||||||
|
function readPath(option: object, path: string) {
|
||||||
|
return path.split('.').reduce<unknown>((value, key) => {
|
||||||
|
if (typeof value !== 'object' || value === null) return undefined
|
||||||
|
return (value as Record<string, unknown>)[key]
|
||||||
|
}, option)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLabel(option: object, accessor?: CSelectLabelAccessor) {
|
||||||
|
const value = accessor
|
||||||
|
? typeof accessor === 'function'
|
||||||
|
? accessor(option)
|
||||||
|
: readPath(option, accessor)
|
||||||
|
: readPath(option, 'label')
|
||||||
|
return value === undefined || value === null ? '' : String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveValue(option: object, accessor?: CSelectValueAccessor): CSelectValue {
|
||||||
|
if (!accessor) return option
|
||||||
|
return typeof accessor === 'function'
|
||||||
|
? accessor(option)
|
||||||
|
: (readPath(option, accessor) as CSelectValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectKey(value: object) {
|
||||||
|
let key = objectKeys.get(value)
|
||||||
|
if (key === undefined) {
|
||||||
|
key = nextObjectKey++
|
||||||
|
objectKeys.set(value, key)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleOptions = computed(() =>
|
||||||
|
props.options.flatMap((source, index) => {
|
||||||
|
if (props.optionLabel || props.optionValue) {
|
||||||
|
const record = source as Record<string, unknown>
|
||||||
|
if (record.hidden) return []
|
||||||
|
const value = resolveValue(source, props.optionValue)
|
||||||
|
|
||||||
|
const explicitKey = props.optionKey
|
||||||
|
? typeof props.optionKey === 'function'
|
||||||
|
? props.optionKey(source)
|
||||||
|
: readPath(source, props.optionKey)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key:
|
||||||
|
typeof explicitKey === 'string' || typeof explicitKey === 'number'
|
||||||
|
? explicitKey
|
||||||
|
: typeof value === 'object'
|
||||||
|
? `object:${objectKey(source)}`
|
||||||
|
: `mapped:${typeof value}:${String(value)}:${index}`,
|
||||||
|
label: resolveLabel(source, props.optionLabel),
|
||||||
|
value,
|
||||||
|
disabled: Boolean(record.disabled),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const option = source as CSelectOption
|
||||||
|
if (option.hidden) return []
|
||||||
|
|
||||||
|
const valueKey =
|
||||||
|
typeof option.value === 'object'
|
||||||
|
? `object:${objectKey(option.value)}`
|
||||||
|
: `${typeof option.value}:${String(option.value)}:${index}`
|
||||||
|
|
||||||
|
return [{ ...option, key: valueKey }]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<select
|
||||||
|
v-model="selection"
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="c-select"
|
||||||
|
:class="[
|
||||||
|
`is-${size}`,
|
||||||
|
{
|
||||||
|
'is-invalid': invalid,
|
||||||
|
'has-placeholder': Boolean(placeholder) && (modelValue ?? null) === null,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
:id="controlId"
|
||||||
|
:disabled="disabled"
|
||||||
|
:required="required"
|
||||||
|
:aria-invalid="invalid ? 'true' : undefined"
|
||||||
|
:aria-describedby="describedBy"
|
||||||
|
>
|
||||||
|
<option v-if="placeholder" :value="null" disabled>{{ placeholder }}</option>
|
||||||
|
<option
|
||||||
|
v-for="option in visibleOptions"
|
||||||
|
:key="option.key"
|
||||||
|
:value="option.value"
|
||||||
|
:disabled="option.disabled"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</option>
|
||||||
|
<slot />
|
||||||
|
</select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-select {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
height: 30px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 4px 28px 4px 7px;
|
||||||
|
color: var(--c-text-color, #20242a);
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.2;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--c-input-background, #fff);
|
||||||
|
border: 1px solid var(--c-control-border-color, #bfc5ce);
|
||||||
|
border-radius: var(--c-border-radius, 3px);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: var(--c-control-hover-border-color, #929aa6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
border-color: var(--c-focus-color, #3578c6);
|
||||||
|
outline: 1px solid var(--c-focus-color, #3578c6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--c-disabled-text-color, #8a9099);
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: var(--c-disabled-background-color, #f1f3f5);
|
||||||
|
border-color: var(--c-disabled-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.has-placeholder {
|
||||||
|
color: var(--c-placeholder-color, #7a828d);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-invalid {
|
||||||
|
border-color: var(--c-danger-color, #b42318);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline-color: var(--c-danger-color, #b42318);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-small {
|
||||||
|
height: 26px;
|
||||||
|
padding-block: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-large {
|
||||||
|
height: 34px;
|
||||||
|
padding-block: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
export type CSelectPrimitive = string | number | boolean
|
||||||
|
export type CSelectValue = CSelectPrimitive | object
|
||||||
|
|
||||||
|
export interface CSelectOption {
|
||||||
|
label: string
|
||||||
|
value: CSelectValue
|
||||||
|
disabled?: boolean
|
||||||
|
hidden?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CSelectLabelAccessor = string | ((option: object) => string)
|
||||||
|
export type CSelectKeyAccessor = string | ((option: object) => string | number)
|
||||||
|
export type CSelectValueAccessor = string | ((option: object) => CSelectValue)
|
||||||
@ -0,0 +1,113 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useFormControl } from '../form/useFormControl'
|
||||||
|
import type { CFormControlSize } from '../form/types'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: string | null
|
||||||
|
size?: CFormControlSize
|
||||||
|
disabled?: boolean
|
||||||
|
readonly?: boolean
|
||||||
|
required?: boolean
|
||||||
|
invalid?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: '',
|
||||||
|
size: 'medium',
|
||||||
|
disabled: false,
|
||||||
|
readonly: false,
|
||||||
|
required: false,
|
||||||
|
invalid: false,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { controlId, describedBy, invalid, required } = useFormControl(props)
|
||||||
|
|
||||||
|
function updateValue(event: Event) {
|
||||||
|
emit('update:modelValue', (event.target as HTMLTextAreaElement).value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<textarea
|
||||||
|
v-bind="$attrs"
|
||||||
|
class="c-text-area"
|
||||||
|
:class="[`is-${size}`, { 'is-invalid': invalid }]"
|
||||||
|
:id="controlId"
|
||||||
|
:value="modelValue ?? ''"
|
||||||
|
:disabled="disabled"
|
||||||
|
:readonly="readonly"
|
||||||
|
:required="required"
|
||||||
|
:aria-invalid="invalid ? 'true' : undefined"
|
||||||
|
:aria-describedby="describedBy"
|
||||||
|
@input="updateValue"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.c-text-area {
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 64px;
|
||||||
|
padding: 6px 7px;
|
||||||
|
color: var(--c-text-color, #20242a);
|
||||||
|
font: inherit;
|
||||||
|
line-height: 1.4;
|
||||||
|
resize: vertical;
|
||||||
|
background: var(--c-input-background, #fff);
|
||||||
|
border: 1px solid var(--c-control-border-color, #bfc5ce);
|
||||||
|
border-radius: var(--c-border-radius, 3px);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--c-placeholder-color, #7a828d);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover:not(:disabled):not(:read-only) {
|
||||||
|
border-color: var(--c-control-hover-border-color, #929aa6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--c-focus-color, #3578c6);
|
||||||
|
outline: 1px solid var(--c-focus-color, #3578c6);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: var(--c-disabled-text-color, #8a9099);
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: var(--c-disabled-background-color, #f1f3f5);
|
||||||
|
border-color: var(--c-disabled-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:read-only:not(:disabled) {
|
||||||
|
background: var(--c-readonly-background-color, #f7f8fa);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-invalid {
|
||||||
|
border-color: var(--c-danger-color, #b42318);
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline-color: var(--c-danger-color, #b42318);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-small {
|
||||||
|
min-height: 52px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-large {
|
||||||
|
min-height: 76px;
|
||||||
|
padding: 7px 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,129 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { CCheckbox, CFormField, CSeparator } from '@/index'
|
||||||
|
import type { CCheckboxValue } from '@/index'
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
|
||||||
|
const enabled = ref<CCheckboxValue | CCheckboxValue[]>(true)
|
||||||
|
const permissions = ref<CCheckboxValue | CCheckboxValue[]>(['read', 'reports'])
|
||||||
|
const accepted = ref<CCheckboxValue | CCheckboxValue[]>(false)
|
||||||
|
const smallChecked = ref<CCheckboxValue | CCheckboxValue[]>(true)
|
||||||
|
const largeChecked = ref<CCheckboxValue | CCheckboxValue[]>(true)
|
||||||
|
const approval = ref<boolean | null>(false)
|
||||||
|
|
||||||
|
const basicUsage = `<CCheckbox v-model="enabled">Enable notifications</CCheckbox>`
|
||||||
|
const groupUsage = `<CCheckbox v-model="permissions" value="read">Read</CCheckbox>
|
||||||
|
<CCheckbox v-model="permissions" value="write">Write</CCheckbox>
|
||||||
|
<CCheckbox v-model="permissions" value="reports">Reports</CCheckbox>`
|
||||||
|
const valueUsage = `<CCheckbox
|
||||||
|
v-model="status"
|
||||||
|
true-value="enabled"
|
||||||
|
false-value="disabled"
|
||||||
|
>
|
||||||
|
Account enabled
|
||||||
|
</CCheckbox>`
|
||||||
|
const threeStateUsage = `<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const approval = ref<boolean | null>(false)
|
||||||
|
<\/script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CCheckbox v-model="approval" three-state>
|
||||||
|
Approval state
|
||||||
|
</CCheckbox>
|
||||||
|
</template>`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="form-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div><p class="category">Forms</p><h1>Checkbox</h1></div>
|
||||||
|
<p>
|
||||||
|
<code>CCheckbox</code> supports boolean choices, custom checked values, array groups, and
|
||||||
|
indeterminate presentation while retaining a native checkbox input.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Boolean value</h2>
|
||||||
|
<div class="preview">
|
||||||
|
<CCheckbox v-model="enabled">Enable notifications</CCheckbox>
|
||||||
|
<span>Model: {{ enabled }}</span>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="basicUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Array group</h2>
|
||||||
|
<p>
|
||||||
|
When the model is an array, each checkbox adds or removes its <code>value</code>. This is
|
||||||
|
useful for permissions and independent feature selections.
|
||||||
|
</p>
|
||||||
|
<div class="preview">
|
||||||
|
<CCheckbox v-model="permissions" value="read">Read</CCheckbox>
|
||||||
|
<CCheckbox v-model="permissions" value="write">Write</CCheckbox>
|
||||||
|
<CCheckbox v-model="permissions" value="reports">Reports</CCheckbox>
|
||||||
|
<span>Selected: {{ permissions }}</span>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="groupUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>States and sizes</h2>
|
||||||
|
<div class="preview">
|
||||||
|
<CCheckbox v-model="smallChecked" size="small">Small</CCheckbox>
|
||||||
|
<CCheckbox v-model="largeChecked" size="large">Large</CCheckbox>
|
||||||
|
<CCheckbox indeterminate>Partially selected</CCheckbox>
|
||||||
|
<CCheckbox disabled>Disabled</CCheckbox>
|
||||||
|
<CFormField error="You must accept the policy.">
|
||||||
|
<CCheckbox v-model="accepted">Accept policy</CCheckbox>
|
||||||
|
</CFormField>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Custom values</h2>
|
||||||
|
<p>
|
||||||
|
<code>true-value</code> and <code>false-value</code> replace the default boolean values for
|
||||||
|
scalar models.
|
||||||
|
</p>
|
||||||
|
<CCodeBlock class="code-sample" :code="valueUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Three-state checkbox</h2>
|
||||||
|
<p>
|
||||||
|
Add <code>three-state</code> to cycle through unchecked, checked, and indeterminate values.
|
||||||
|
The model sequence is <code>false → true → null → false</code>, where <code>null</code>
|
||||||
|
represents the indeterminate state. Three-state behavior applies to scalar models, not
|
||||||
|
array groups.
|
||||||
|
</p>
|
||||||
|
<div class="preview">
|
||||||
|
<CCheckbox v-model="approval" three-state>Approval state</CCheckbox>
|
||||||
|
<span>Model: {{ approval === null ? 'null (indeterminate)' : approval }}</span>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="threeStateUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Properties</h2>
|
||||||
|
<dl class="property-list">
|
||||||
|
<div><dt><code>model-value</code></dt><dd>A scalar checkbox value or an array used by <code>v-model</code>.</dd></div>
|
||||||
|
<div><dt><code>value</code></dt><dd>Value added to or removed from an array model.</dd></div>
|
||||||
|
<div><dt><code>true-value</code></dt><dd>Scalar value emitted when checked. Defaults to true.</dd></div>
|
||||||
|
<div><dt><code>false-value</code></dt><dd>Scalar value emitted when unchecked. Defaults to false.</dd></div>
|
||||||
|
<div><dt><code>indeterminate</code></dt><dd>Displays a mixed state without changing the model by itself.</dd></div>
|
||||||
|
<div><dt><code>three-state</code></dt><dd>Cycles a scalar model through false, true, and null.</dd></div>
|
||||||
|
<div><dt><code>size</code></dt><dd><code>small</code>, <code>medium</code>, or <code>large</code>.</dd></div>
|
||||||
|
<div><dt><code>disabled</code></dt><dd>Disables interaction.</dd></div>
|
||||||
|
<div><dt><code>required</code></dt><dd>Applies native required validation.</dd></div>
|
||||||
|
<div><dt><code>invalid</code></dt><dd>Applies invalid styling and <code>aria-invalid</code>.</dd></div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss" src="./form-demo.scss"></style>
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { CFormField, CInput, CSeparator } from '@/index'
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
|
||||||
|
const customer = ref('Contoso Ltd.')
|
||||||
|
const email = ref('invalid-address')
|
||||||
|
|
||||||
|
const basicUsage = `<CFormField
|
||||||
|
label="Customer"
|
||||||
|
hint="Enter the registered company name."
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<CInput v-model="customer" />
|
||||||
|
</CFormField>`
|
||||||
|
|
||||||
|
const errorUsage = `<CFormField
|
||||||
|
label="Email"
|
||||||
|
error="Enter a valid email address."
|
||||||
|
>
|
||||||
|
<CInput v-model="email" type="email" />
|
||||||
|
</CFormField>`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="form-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div><p class="category">Forms</p><h1>Form Field</h1></div>
|
||||||
|
<p>
|
||||||
|
<code>CFormField</code> consistently connects a label, hint, validation message, and
|
||||||
|
required state to a nested Concise UI form control.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Label and hint</h2>
|
||||||
|
<p>
|
||||||
|
The field generates an input ID and accessible description automatically. Use
|
||||||
|
<code>for</code> only when a specific ID is required.
|
||||||
|
</p>
|
||||||
|
<div class="preview">
|
||||||
|
<CFormField label="Customer" hint="Enter the registered company name." required>
|
||||||
|
<CInput v-model="customer" />
|
||||||
|
</CFormField>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="basicUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Validation message</h2>
|
||||||
|
<p>
|
||||||
|
Providing <code>error</code> marks the nested control invalid, connects the message with
|
||||||
|
<code>aria-describedby</code>, and announces it as an alert.
|
||||||
|
</p>
|
||||||
|
<div class="preview">
|
||||||
|
<CFormField label="Email" error="Enter a valid email address.">
|
||||||
|
<CInput v-model="email" type="email" />
|
||||||
|
</CFormField>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="errorUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Properties</h2>
|
||||||
|
<dl class="property-list">
|
||||||
|
<div><dt><code>label</code></dt><dd>Text displayed above the control.</dd></div>
|
||||||
|
<div><dt><code>for</code></dt><dd>Optional control ID; generated automatically by default.</dd></div>
|
||||||
|
<div><dt><code>hint</code></dt><dd>Supporting text connected to the control.</dd></div>
|
||||||
|
<div><dt><code>error</code></dt><dd>Validation message that also marks the field invalid.</dd></div>
|
||||||
|
<div><dt><code>required</code></dt><dd>Shows a required marker and marks the nested control required.</dd></div>
|
||||||
|
<div><dt><code>invalid</code></dt><dd>Marks the nested control invalid without requiring an error string.</dd></div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss" src="./form-demo.scss"></style>
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { CFormField, CInput, CSeparator } from '@/index'
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
|
||||||
|
const name = ref('Ada Lovelace')
|
||||||
|
const search = ref('')
|
||||||
|
|
||||||
|
const basicUsage = `<CInput v-model="name" placeholder="Full name" />
|
||||||
|
<CInput v-model="search" type="search" placeholder="Search records" />`
|
||||||
|
const sizeUsage = `<CInput size="small" placeholder="Small" />
|
||||||
|
<CInput size="medium" placeholder="Medium" />
|
||||||
|
<CInput size="large" placeholder="Large" />`
|
||||||
|
const stateUsage = `<CInput disabled model-value="Unavailable" />
|
||||||
|
<CInput readonly model-value="Read-only value" />
|
||||||
|
<CInput invalid model-value="Invalid value" />`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="form-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div><p class="category">Forms</p><h1>Input</h1></div>
|
||||||
|
<p>
|
||||||
|
<code>CInput</code> is a compact native input with <code>v-model</code>, consistent sizing,
|
||||||
|
validation styling, and forwarded HTML attributes.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Basic usage</h2>
|
||||||
|
<div class="preview">
|
||||||
|
<CFormField label="Full name"><CInput v-model="name" /></CFormField>
|
||||||
|
<CFormField label="Search"><CInput v-model="search" type="search" placeholder="Search records" /></CFormField>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="basicUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Sizes</h2>
|
||||||
|
<div class="preview-row">
|
||||||
|
<CInput size="small" placeholder="Small" aria-label="Small input" />
|
||||||
|
<CInput size="medium" placeholder="Medium" aria-label="Medium input" />
|
||||||
|
<CInput size="large" placeholder="Large" aria-label="Large input" />
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="sizeUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>States</h2>
|
||||||
|
<div class="preview">
|
||||||
|
<CInput disabled model-value="Unavailable" aria-label="Disabled input" />
|
||||||
|
<CInput readonly model-value="Read-only value" aria-label="Read-only input" />
|
||||||
|
<CInput invalid model-value="Invalid value" aria-label="Invalid input" />
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="stateUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Properties</h2>
|
||||||
|
<dl class="property-list">
|
||||||
|
<div><dt><code>model-value</code></dt><dd>String value used by <code>v-model</code>.</dd></div>
|
||||||
|
<div><dt><code>type</code></dt><dd>Any native HTML input type. Defaults to <code>text</code>.</dd></div>
|
||||||
|
<div><dt><code>size</code></dt><dd><code>small</code>, <code>medium</code>, or <code>large</code>.</dd></div>
|
||||||
|
<div><dt><code>disabled</code></dt><dd>Prevents editing and focus.</dd></div>
|
||||||
|
<div><dt><code>readonly</code></dt><dd>Prevents editing while keeping the value focusable.</dd></div>
|
||||||
|
<div><dt><code>required</code></dt><dd>Applies native required validation.</dd></div>
|
||||||
|
<div><dt><code>invalid</code></dt><dd>Applies invalid styling and <code>aria-invalid</code>.</dd></div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss" src="./form-demo.scss"></style>
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
CButton,
|
||||||
|
CFormField,
|
||||||
|
CIcon,
|
||||||
|
CInput,
|
||||||
|
CInputAddon,
|
||||||
|
CInputGroup,
|
||||||
|
CNumberInput,
|
||||||
|
CPassword,
|
||||||
|
CSeparator,
|
||||||
|
} from '@/index'
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
|
||||||
|
const search = ref('')
|
||||||
|
const password = ref('warehouse-2026')
|
||||||
|
const price = ref<number | null>(49.95)
|
||||||
|
const lastAction = ref('No action selected')
|
||||||
|
|
||||||
|
const searchUsage = `<CFormField label="Search">
|
||||||
|
<CInputGroup>
|
||||||
|
<CInputAddon><CIcon>⌕</CIcon></CInputAddon>
|
||||||
|
<CInput v-model="search" />
|
||||||
|
<CButton variant="primary">Search</CButton>
|
||||||
|
</CInputGroup>
|
||||||
|
</CFormField>`
|
||||||
|
|
||||||
|
const compositionUsage = `<CInputGroup>
|
||||||
|
<CPassword v-model="password" />
|
||||||
|
<CButton>Generate</CButton>
|
||||||
|
</CInputGroup>
|
||||||
|
|
||||||
|
<CInputGroup>
|
||||||
|
<CInputAddon>$</CInputAddon>
|
||||||
|
<CNumberInput v-model="price" :controls="false" />
|
||||||
|
<CInputAddon>USD</CInputAddon>
|
||||||
|
</CInputGroup>`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="form-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div><p class="category">Forms</p><h1>Input Group</h1></div>
|
||||||
|
<p>
|
||||||
|
<code>CInputGroup</code> joins inputs, addons, and buttons into one compact control without
|
||||||
|
adding component-specific prefix and suffix props.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Icon and action</h2>
|
||||||
|
<p>
|
||||||
|
Place children in visual order. Use <code>CInputAddon</code> for non-interactive icons or
|
||||||
|
text, and a real <code>CButton</code> for actions.
|
||||||
|
</p>
|
||||||
|
<div class="preview">
|
||||||
|
<CFormField label="Search records">
|
||||||
|
<CInputGroup>
|
||||||
|
<CInputAddon><CIcon>⌕</CIcon></CInputAddon>
|
||||||
|
<CInput v-model="search" placeholder="Order or customer" />
|
||||||
|
<CButton variant="primary" @click="lastAction = `Searched for ${search || 'everything'}`">
|
||||||
|
Search
|
||||||
|
</CButton>
|
||||||
|
</CInputGroup>
|
||||||
|
</CFormField>
|
||||||
|
<span class="status" aria-live="polite">{{ lastAction }}</span>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="searchUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Compound controls</h2>
|
||||||
|
<p>
|
||||||
|
Password and number inputs keep their own internal controls. Additional addons and buttons
|
||||||
|
are placed outside them, and the group joins all outer borders automatically.
|
||||||
|
</p>
|
||||||
|
<div class="preview">
|
||||||
|
<CFormField label="Password">
|
||||||
|
<CInputGroup>
|
||||||
|
<CPassword v-model="password" />
|
||||||
|
<CButton @click="lastAction = 'Generated password'">Generate</CButton>
|
||||||
|
</CInputGroup>
|
||||||
|
</CFormField>
|
||||||
|
|
||||||
|
<CFormField label="Unit price">
|
||||||
|
<CInputGroup>
|
||||||
|
<CInputAddon>$</CInputAddon>
|
||||||
|
<CNumberInput v-model="price" :controls="false" />
|
||||||
|
<CInputAddon>USD</CInputAddon>
|
||||||
|
</CInputGroup>
|
||||||
|
</CFormField>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="compositionUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Responsibilities</h2>
|
||||||
|
<dl class="property-list">
|
||||||
|
<div>
|
||||||
|
<dt><code>CInputGroup</code></dt>
|
||||||
|
<dd>Arranges children horizontally, joins adjacent borders, and preserves focus stacking.</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt><code>CInputAddon</code></dt>
|
||||||
|
<dd>Displays non-interactive text, units, or icons with a bordered neutral background.</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Child order</dt>
|
||||||
|
<dd>Determines whether an addon or button appears before or after the input.</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Sizes</dt>
|
||||||
|
<dd>Set the same size on the input, addon, and button when using a non-default size.</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss" src="./form-demo.scss"></style>
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { CFormField, CPassword, CSeparator } from '@/index'
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
|
||||||
|
const password = ref('warehouse-2026')
|
||||||
|
const passwordVisible = ref(false)
|
||||||
|
|
||||||
|
const basicUsage = `<CFormField label="Password" required>
|
||||||
|
<CPassword v-model="password" autocomplete="current-password" />
|
||||||
|
</CFormField>`
|
||||||
|
const visibilityUsage = `<CPassword
|
||||||
|
v-model="password"
|
||||||
|
v-model:visible="passwordVisible"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CPassword v-model="password" :revealable="false" />`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="form-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div><p class="category">Forms</p><h1>Password</h1></div>
|
||||||
|
<p>
|
||||||
|
<code>CPassword</code> combines a native password input with an optional accessible
|
||||||
|
visibility control. Password values remain ordinary strings managed through
|
||||||
|
<code>v-model</code>.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Basic usage</h2>
|
||||||
|
<div class="preview">
|
||||||
|
<CFormField label="Password" required>
|
||||||
|
<CPassword v-model="password" autocomplete="current-password" />
|
||||||
|
</CFormField>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="basicUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Visibility control</h2>
|
||||||
|
<p>
|
||||||
|
Visibility is managed internally by default. Use <code>v-model:visible</code> when the
|
||||||
|
application needs to observe or control it, or disable the toggle with
|
||||||
|
<code>:revealable="false"</code>.
|
||||||
|
</p>
|
||||||
|
<div class="preview">
|
||||||
|
<CPassword v-model="password" v-model:visible="passwordVisible" aria-label="Controlled password" />
|
||||||
|
<CPassword v-model="password" :revealable="false" aria-label="Password without reveal button" />
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="visibilityUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Sizes and states</h2>
|
||||||
|
<div class="preview">
|
||||||
|
<CPassword size="small" model-value="secret" aria-label="Small password" />
|
||||||
|
<CPassword size="large" model-value="secret" aria-label="Large password" />
|
||||||
|
<CPassword disabled model-value="secret" aria-label="Disabled password" />
|
||||||
|
<CPassword invalid model-value="secret" aria-label="Invalid password" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Properties</h2>
|
||||||
|
<dl class="property-list">
|
||||||
|
<div><dt><code>model-value</code></dt><dd>Password string used by <code>v-model</code>.</dd></div>
|
||||||
|
<div><dt><code>visible</code></dt><dd>Optional controlled visibility state used by <code>v-model:visible</code>.</dd></div>
|
||||||
|
<div><dt><code>revealable</code></dt><dd>Shows the visibility button when true. Defaults to true.</dd></div>
|
||||||
|
<div><dt><code>size</code></dt><dd><code>small</code>, <code>medium</code>, or <code>large</code>.</dd></div>
|
||||||
|
<div><dt><code>disabled</code></dt><dd>Disables the input and visibility button.</dd></div>
|
||||||
|
<div><dt><code>readonly</code></dt><dd>Prevents editing while allowing visibility changes.</dd></div>
|
||||||
|
<div><dt><code>required</code></dt><dd>Applies native required validation.</dd></div>
|
||||||
|
<div><dt><code>invalid</code></dt><dd>Applies invalid styling and <code>aria-invalid</code>.</dd></div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss" src="./form-demo.scss"></style>
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { CRadio, CSeparator } from '@/index'
|
||||||
|
import type { CRadioValue } from '@/index'
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
|
||||||
|
const shipping = ref<CRadioValue | null>('standard')
|
||||||
|
|
||||||
|
const groupUsage = `<fieldset>
|
||||||
|
<legend>Shipping method</legend>
|
||||||
|
<CRadio v-model="shipping" name="shipping" value="standard">Standard</CRadio>
|
||||||
|
<CRadio v-model="shipping" name="shipping" value="express">Express</CRadio>
|
||||||
|
<CRadio v-model="shipping" name="shipping" value="pickup">Pickup</CRadio>
|
||||||
|
</fieldset>`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="form-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div><p class="category">Forms</p><h1>Radio</h1></div>
|
||||||
|
<p>
|
||||||
|
<code>CRadio</code> represents one mutually exclusive choice. Radios sharing a model and
|
||||||
|
native <code>name</code> form a keyboard-accessible group.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Radio group</h2>
|
||||||
|
<p>
|
||||||
|
Use a native <code>fieldset</code> and <code>legend</code> to give the complete group an
|
||||||
|
accessible label. Each radio emits its own <code>value</code> when selected.
|
||||||
|
</p>
|
||||||
|
<div class="preview">
|
||||||
|
<fieldset class="radio-group">
|
||||||
|
<legend>Shipping method</legend>
|
||||||
|
<CRadio v-model="shipping" name="shipping" value="standard">Standard</CRadio>
|
||||||
|
<CRadio v-model="shipping" name="shipping" value="express">Express</CRadio>
|
||||||
|
<CRadio v-model="shipping" name="shipping" value="pickup">Pickup</CRadio>
|
||||||
|
</fieldset>
|
||||||
|
<span>Selected: {{ shipping }}</span>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="groupUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Sizes and states</h2>
|
||||||
|
<div class="preview">
|
||||||
|
<CRadio :model-value="1" :value="1" size="small" name="small-example">Small</CRadio>
|
||||||
|
<CRadio :model-value="1" :value="1" size="large" name="large-example">Large</CRadio>
|
||||||
|
<CRadio :model-value="false" :value="true" disabled name="disabled-example">Disabled</CRadio>
|
||||||
|
<CRadio :model-value="true" :value="true" invalid name="invalid-example">Invalid</CRadio>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Properties</h2>
|
||||||
|
<dl class="property-list">
|
||||||
|
<div><dt><code>model-value</code></dt><dd>Currently selected group value, used by <code>v-model</code>.</dd></div>
|
||||||
|
<div><dt><code>value</code></dt><dd>Required value emitted when this radio is selected.</dd></div>
|
||||||
|
<div><dt><code>size</code></dt><dd><code>small</code>, <code>medium</code>, or <code>large</code>.</dd></div>
|
||||||
|
<div><dt><code>disabled</code></dt><dd>Prevents this option from being selected.</dd></div>
|
||||||
|
<div><dt><code>required</code></dt><dd>Applies native required validation.</dd></div>
|
||||||
|
<div><dt><code>invalid</code></dt><dd>Applies invalid styling and <code>aria-invalid</code>.</dd></div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@use './form-demo.scss';
|
||||||
|
|
||||||
|
.radio-group {
|
||||||
|
display: grid;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 10px 10px;
|
||||||
|
gap: 7px;
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
legend {
|
||||||
|
padding: 0 4px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -0,0 +1,186 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { CFormField, CSelect, CSeparator } from '@/index'
|
||||||
|
import type { CSelectOption, CSelectValue } from '@/index'
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
|
||||||
|
const warehouse = ref<CSelectValue | null>('north')
|
||||||
|
const priority = ref<CSelectValue | null>(null)
|
||||||
|
const status = ref<CSelectValue | null>('active')
|
||||||
|
|
||||||
|
const products = [
|
||||||
|
{ id: 1, name: 'Book', code: 'BOO' },
|
||||||
|
{ id: 2, name: 'Stove', code: 'STV' },
|
||||||
|
]
|
||||||
|
const selectedProduct = ref<(typeof products)[number] | null>(products[0] ?? null)
|
||||||
|
const selectedProductId = ref<number | null>(1)
|
||||||
|
|
||||||
|
const warehouses: CSelectOption[] = [
|
||||||
|
{ label: 'North warehouse', value: 'north' },
|
||||||
|
{ label: 'Central warehouse', value: 'central' },
|
||||||
|
{ label: 'South warehouse', value: 'south', disabled: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
const dataJavaScript = `const products = [
|
||||||
|
{ id: 1, name: 'Book', code: 'BOO' },
|
||||||
|
{ id: 2, name: 'Stove', code: 'STV' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const statuses = [
|
||||||
|
{ label: 'Active', value: 'active' },
|
||||||
|
{ label: 'Inactive', value: 'inactive' },
|
||||||
|
]`
|
||||||
|
|
||||||
|
const statuses: CSelectOption[] = [
|
||||||
|
{ label: 'Active', value: 'active' },
|
||||||
|
{ label: 'Inactive', value: 'inactive' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const dataUsage = `<CSelect
|
||||||
|
v-model="selectedProductId"
|
||||||
|
:options="products"
|
||||||
|
option-label="name"
|
||||||
|
option-value="id"
|
||||||
|
option-key="id"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- label and value are interpreted automatically -->
|
||||||
|
<CSelect
|
||||||
|
v-model="status"
|
||||||
|
:options="statuses"
|
||||||
|
/>`
|
||||||
|
|
||||||
|
const slotUsage = `<CSelect v-model="priority" placeholder="Select priority">
|
||||||
|
<option value="normal">Normal</option>
|
||||||
|
<option value="urgent">Urgent</option>
|
||||||
|
</CSelect>`
|
||||||
|
|
||||||
|
const objectJavaScript = `const products = [
|
||||||
|
{ id: 1, name: 'Book', code: 'BOO' },
|
||||||
|
{ id: 2, name: 'Stove', code: 'STV' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const selectedProduct = ref(null)`
|
||||||
|
|
||||||
|
const objectUsage = `<CSelect
|
||||||
|
v-model="selectedProduct"
|
||||||
|
:options="products"
|
||||||
|
option-label="name"
|
||||||
|
option-key="id"
|
||||||
|
/>`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="form-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div><p class="category">Forms</p><h1>Select</h1></div>
|
||||||
|
<p>
|
||||||
|
<code>CSelect</code> styles a native single-value select and supports either data-driven
|
||||||
|
options or ordinary HTML option slots.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Object options</h2>
|
||||||
|
<p>
|
||||||
|
Pass raw objects with <code>option-label</code> to choose the displayed field. When
|
||||||
|
<code>option-value</code> is omitted, selecting an option binds the complete object. Use
|
||||||
|
<code>option-key</code> to provide stable rendering identity.
|
||||||
|
</p>
|
||||||
|
<div class="preview">
|
||||||
|
<CFormField label="Product">
|
||||||
|
<CSelect
|
||||||
|
v-model="selectedProduct"
|
||||||
|
:options="products"
|
||||||
|
option-label="name"
|
||||||
|
option-key="id"
|
||||||
|
/>
|
||||||
|
</CFormField>
|
||||||
|
<span>Selected object: {{ selectedProduct }}</span>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="objectJavaScript" language="javascript" />
|
||||||
|
<CCodeBlock class="code-sample" :code="objectUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Data-driven options</h2>
|
||||||
|
<p>
|
||||||
|
For raw objects, <code>option-label</code> selects the text shown to the user and
|
||||||
|
<code>option-value</code> selects the field written to <code>v-model</code>. Both accept a
|
||||||
|
property path or function. If <code>option-value</code> is omitted, the complete object is
|
||||||
|
used as shown above.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
When every option already has <code>label</code> and <code>value</code> fields,
|
||||||
|
<code>CSelect</code> interprets those fields automatically—no accessor props are needed.
|
||||||
|
These structured options can also include <code>disabled</code> and <code>hidden</code>.
|
||||||
|
</p>
|
||||||
|
<div class="preview">
|
||||||
|
<CFormField label="Product ID">
|
||||||
|
<CSelect
|
||||||
|
v-model="selectedProductId"
|
||||||
|
:options="products"
|
||||||
|
option-label="name"
|
||||||
|
option-value="id"
|
||||||
|
option-key="id"
|
||||||
|
/>
|
||||||
|
</CFormField>
|
||||||
|
<span>Mapped value: {{ selectedProductId }}</span>
|
||||||
|
|
||||||
|
<CFormField label="Status">
|
||||||
|
<CSelect v-model="status" :options="statuses" />
|
||||||
|
</CFormField>
|
||||||
|
<span>Automatic value: {{ status }}</span>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="dataJavaScript" language="javascript" />
|
||||||
|
<CCodeBlock class="code-sample" :code="dataUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Native option slot</h2>
|
||||||
|
<p>
|
||||||
|
Use the default slot when native <code>option</code> or <code>optgroup</code> markup is more
|
||||||
|
convenient. Data-driven and slotted options can also be combined.
|
||||||
|
</p>
|
||||||
|
<div class="preview">
|
||||||
|
<CFormField label="Priority">
|
||||||
|
<CSelect v-model="priority" placeholder="Select priority">
|
||||||
|
<option value="normal">Normal</option>
|
||||||
|
<option value="urgent">Urgent</option>
|
||||||
|
</CSelect>
|
||||||
|
</CFormField>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="slotUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Sizes and states</h2>
|
||||||
|
<div class="preview">
|
||||||
|
<CFormField label="Small (26px)"><CSelect v-model="warehouse" :options="warehouses" size="small" /></CFormField>
|
||||||
|
<CFormField label="Large (34px)"><CSelect v-model="warehouse" :options="warehouses" size="large" /></CFormField>
|
||||||
|
<CFormField label="Disabled"><CSelect :model-value="'north'" :options="warehouses" disabled /></CFormField>
|
||||||
|
<CFormField label="Invalid"><CSelect v-model="warehouse" :options="warehouses" invalid /></CFormField>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Properties</h2>
|
||||||
|
<dl class="property-list">
|
||||||
|
<div><dt><code>model-value</code></dt><dd>Selected primitive, object, or <code>null</code>, used by <code>v-model</code>.</dd></div>
|
||||||
|
<div><dt><code>options</code></dt><dd>Array of <code>{ label, value }</code> records or raw objects.</dd></div>
|
||||||
|
<div><dt><code>option-label</code></dt><dd>Property path or function used to label raw object options.</dd></div>
|
||||||
|
<div><dt><code>option-value</code></dt><dd>Property path or function selecting the bound value. Omit it to bind the complete object.</dd></div>
|
||||||
|
<div><dt><code>option-key</code></dt><dd>Property path or function providing a stable string or number key for raw objects.</dd></div>
|
||||||
|
<div><dt><code>placeholder</code></dt><dd>Disabled initial option displayed while the model is null.</dd></div>
|
||||||
|
<div><dt><code>size</code></dt><dd><code>small</code>, <code>medium</code>, or <code>large</code>.</dd></div>
|
||||||
|
<div><dt><code>disabled</code></dt><dd>Disables selection and focus.</dd></div>
|
||||||
|
<div><dt><code>required</code></dt><dd>Applies native required validation.</dd></div>
|
||||||
|
<div><dt><code>invalid</code></dt><dd>Applies invalid styling and <code>aria-invalid</code>.</dd></div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss" src="./form-demo.scss"></style>
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import { CFormField, CSeparator, CTextArea } from '@/index'
|
||||||
|
import CCodeBlock from '@/documentation/CCodeBlock.vue'
|
||||||
|
|
||||||
|
const notes = ref('Deliver to receiving dock 3.')
|
||||||
|
|
||||||
|
const basicUsage = `<CFormField label="Delivery notes" hint="Include access or handling instructions.">
|
||||||
|
<CTextArea v-model="notes" rows="4" />
|
||||||
|
</CFormField>`
|
||||||
|
const stateUsage = `<CTextArea readonly model-value="Read-only notes" />
|
||||||
|
<CTextArea disabled model-value="Unavailable" />
|
||||||
|
<CTextArea invalid model-value="Review this value" />`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<article class="form-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<div><p class="category">Forms</p><h1>Text Area</h1></div>
|
||||||
|
<p>
|
||||||
|
<code>CTextArea</code> provides compact multiline text entry while preserving native rows,
|
||||||
|
length constraints, selection, and resize behavior.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<CSeparator />
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Basic usage</h2>
|
||||||
|
<div class="preview">
|
||||||
|
<CFormField label="Delivery notes" hint="Include access or handling instructions.">
|
||||||
|
<CTextArea v-model="notes" rows="4" />
|
||||||
|
</CFormField>
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="basicUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Sizes</h2>
|
||||||
|
<p>The size controls minimum height, padding, and typography. Native <code>rows</code> remains available.</p>
|
||||||
|
<div class="preview">
|
||||||
|
<CTextArea size="small" placeholder="Small" aria-label="Small text area" />
|
||||||
|
<CTextArea size="medium" placeholder="Medium" aria-label="Medium text area" />
|
||||||
|
<CTextArea size="large" placeholder="Large" aria-label="Large text area" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>States</h2>
|
||||||
|
<div class="preview">
|
||||||
|
<CTextArea readonly model-value="Read-only notes" aria-label="Read-only notes" />
|
||||||
|
<CTextArea disabled model-value="Unavailable" aria-label="Disabled notes" />
|
||||||
|
<CTextArea invalid model-value="Review this value" aria-label="Invalid notes" />
|
||||||
|
</div>
|
||||||
|
<CCodeBlock class="code-sample" :code="stateUsage" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>Properties</h2>
|
||||||
|
<dl class="property-list">
|
||||||
|
<div><dt><code>model-value</code></dt><dd>String value used by <code>v-model</code>.</dd></div>
|
||||||
|
<div><dt><code>size</code></dt><dd><code>small</code>, <code>medium</code>, or <code>large</code>.</dd></div>
|
||||||
|
<div><dt><code>disabled</code></dt><dd>Prevents editing and focus.</dd></div>
|
||||||
|
<div><dt><code>readonly</code></dt><dd>Prevents editing while keeping the value focusable.</dd></div>
|
||||||
|
<div><dt><code>required</code></dt><dd>Applies native required validation.</dd></div>
|
||||||
|
<div><dt><code>invalid</code></dt><dd>Applies invalid styling and <code>aria-invalid</code>.</dd></div>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss" src="./form-demo.scss"></style>
|
||||||
@ -0,0 +1,110 @@
|
|||||||
|
.form-page {
|
||||||
|
width: min(100%, 900px);
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(220px, 0.7fr) minmax(320px, 1.3fr);
|
||||||
|
align-items: end;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
gap: 32px;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
color: var(--c-primary-color, #2f6fad);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 22px 0;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: var(--c-muted-text-color, #626a75);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview,
|
||||||
|
.preview-row {
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--c-surface-color, #fff);
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
display: grid;
|
||||||
|
max-width: 520px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-sample {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-list {
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 120px minmax(0, 1fr);
|
||||||
|
|
||||||
|
+ div {
|
||||||
|
border-top: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dt,
|
||||||
|
dd {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dt {
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--c-subtle-surface-color, #f7f8fa);
|
||||||
|
border-inline-end: 1px solid var(--c-border-color, #d5d9df);
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue