diff --git a/packages/primitives/src/components/number-field/number-field-root.vue b/packages/primitives/src/components/number-field/number-field-root.vue index 1a8d98aa..4b06deb1 100644 --- a/packages/primitives/src/components/number-field/number-field-root.vue +++ b/packages/primitives/src/components/number-field/number-field-root.vue @@ -90,7 +90,14 @@ const inputMode = computed(() => { // Replace negative textValue formatted using currencySign: 'accounting' // with a textValue that can be announced using a minus sign. const textValueFormatter = useNumberFormatter(locale, formatOptions); -const textValue = computed(() => (Number.isNaN(modelValue.value) ? '' : textValueFormatter.format(modelValue.value))); +const textValue = computed(() => { + if (Number.isNaN(modelValue.value)) return ''; + + const formatted = textValueFormatter.format(modelValue.value); + if (formatted === 'NaN') return ''; + + return formatted; +}); function validate(val: string) { return numberParser.isValidPartialNumber(val, min.value, max.value); diff --git a/packages/primitives/src/components/number-field/shared.ts b/packages/primitives/src/components/number-field/shared.ts index 3129bc1e..81cfb3d2 100644 --- a/packages/primitives/src/components/number-field/shared.ts +++ b/packages/primitives/src/components/number-field/shared.ts @@ -4,6 +4,7 @@ import { unrefElement, useEventListener } from '@vueuse/core'; import type { MaybeComputedElementRef } from '@vueuse/core'; import { createEventHook, isClient, reactiveComputed } from '@vueuse/shared'; import { NumberFormatter, NumberParser } from '@internationalized/number'; +import { isNullish } from '../../shared'; export function usePressedHold(options: { target?: MaybeComputedElementRef; disabled: Ref }) { const { disabled } = options; @@ -68,7 +69,11 @@ export function useNumberParser(locale: Ref, options: Ref new NumberParser(locale.value, options.value)); } -export function handleDecimalOperation(operator: '-' | '+', value1: number, value2: number): number { +export function handleDecimalOperation(operator: '-' | '+', value1?: number, value2?: number): number { + if (isNullish(value1) || isNullish(value2)) { + return 0; + } + let v1 = value1; let v2 = value2; diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index fd300ed5..b363b30f 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -23,6 +23,7 @@ export * from './label'; export * from './menu'; export * from './menubar'; export * from './navigation-menu'; +export * from './number-field'; export * from './pagination'; export * from './pin-input'; export * from './popover'; diff --git a/packages/ui/src/components/number-field/index.ts b/packages/ui/src/components/number-field/index.ts new file mode 100644 index 00000000..c56b37ea --- /dev/null +++ b/packages/ui/src/components/number-field/index.ts @@ -0,0 +1,9 @@ +import SNumberFieldDecrement from './number-field-decrement.vue'; +import SNumberFieldIncrement from './number-field-increment.vue'; +import SNumberFieldInput from './number-field-input.vue'; +import SNumberFieldRoot from './number-field-root.vue'; +import SNumberField from './number-field.vue'; + +export { SNumberFieldDecrement, SNumberFieldIncrement, SNumberFieldInput, SNumberFieldRoot, SNumberField }; + +export * from './types'; diff --git a/packages/ui/src/components/number-field/number-field-decrement.vue b/packages/ui/src/components/number-field/number-field-decrement.vue new file mode 100644 index 00000000..78cec941 --- /dev/null +++ b/packages/ui/src/components/number-field/number-field-decrement.vue @@ -0,0 +1,31 @@ + + + diff --git a/packages/ui/src/components/number-field/number-field-increment.vue b/packages/ui/src/components/number-field/number-field-increment.vue new file mode 100644 index 00000000..e7756603 --- /dev/null +++ b/packages/ui/src/components/number-field/number-field-increment.vue @@ -0,0 +1,32 @@ + + + diff --git a/packages/ui/src/components/number-field/number-field-input.vue b/packages/ui/src/components/number-field/number-field-input.vue new file mode 100644 index 00000000..db72e15b --- /dev/null +++ b/packages/ui/src/components/number-field/number-field-input.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/ui/src/components/number-field/number-field-root.vue b/packages/ui/src/components/number-field/number-field-root.vue new file mode 100644 index 00000000..a2086ba5 --- /dev/null +++ b/packages/ui/src/components/number-field/number-field-root.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/ui/src/components/number-field/number-field.vue b/packages/ui/src/components/number-field/number-field.vue new file mode 100644 index 00000000..5faed437 --- /dev/null +++ b/packages/ui/src/components/number-field/number-field.vue @@ -0,0 +1,50 @@ + + + diff --git a/packages/ui/src/components/number-field/types.ts b/packages/ui/src/components/number-field/types.ts new file mode 100644 index 00000000..1e829087 --- /dev/null +++ b/packages/ui/src/components/number-field/types.ts @@ -0,0 +1,43 @@ +import type { + ClassValue, + NumberFieldRootEmits, + NumberFieldDecrementProps as _NumberFieldDecrementProps, + NumberFieldIncrementProps as _NumberFieldIncrementProps, + NumberFieldInputProps as _NumberFieldInputProps, + NumberFieldRootProps as _NumberFieldRootProps +} from '@soybean-ui/primitives'; +import type { NumberFieldSlots, ThemeSize } from '@soybean-ui/variants'; + +export interface NumberFieldRootProps extends _NumberFieldRootProps { + size?: ThemeSize; +} + +export interface NumberFieldInputProps extends _NumberFieldInputProps { + size?: ThemeSize; + center?: boolean; +} + +export interface NumberFieldDecrementProps extends _NumberFieldDecrementProps { + size?: ThemeSize; + center?: boolean; + iconClass?: ClassValue; +} + +export interface NumberFieldIncrementProps extends _NumberFieldIncrementProps { + size?: ThemeSize; + center?: boolean; + iconClass?: ClassValue; +} + +export type NumberFieldUi = Partial>; + +export interface NumberFieldProps extends NumberFieldRootProps { + center?: boolean; + disabledDecrement?: boolean; + disabledIncrement?: boolean; + ui?: NumberFieldUi; +} + +export type NumberFieldEmits = NumberFieldRootEmits; + +export type { NumberFieldRootEmits }; diff --git a/packages/variants/src/index.ts b/packages/variants/src/index.ts index 30952a98..5f46b890 100644 --- a/packages/variants/src/index.ts +++ b/packages/variants/src/index.ts @@ -21,6 +21,7 @@ export * from './variants/label'; export * from './variants/menu'; export * from './variants/menubar'; export * from './variants/navigation-menu'; +export * from './variants/number-field'; export * from './variants/pagination'; export * from './variants/pin-input'; export * from './variants/popover'; diff --git a/packages/variants/src/variants/number-field.ts b/packages/variants/src/variants/number-field.ts new file mode 100644 index 00000000..0becdd4d --- /dev/null +++ b/packages/variants/src/variants/number-field.ts @@ -0,0 +1,119 @@ +// @unocss-include +import { tv } from 'tailwind-variants'; + +export const numberFieldVariants = tv({ + slots: { + root: 'flex items-center w-full rounded-md border border-input bg-background focus-within:(border-input ring-2 ring-offset-2 ring-primary) disabled:(cursor-not-allowed opacity-50)', + decrement: `flex h-full shrink-0 items-center justify-center bg-transparent outline-none disabled:(cursor-not-allowed opacity-20)`, + decrementIcon: '', + increment: `flex h-full shrink-0 items-center justify-center bg-transparent outline-none disabled:(cursor-not-allowed opacity-20)`, + incrementIcon: '', + input: [ + `h-full w-full grow bg-transparent`, + `placeholder:text-muted-foreground outline-none disabled:(cursor-not-allowed opacity-50)` + ] + }, + variants: { + size: { + xs: { + decrement: 'p-1', + decrementIcon: 'text-xs', + increment: 'p-1', + incrementIcon: 'text-xs', + input: 'h-6 text-xs' + }, + sm: { + decrement: 'p-1.25', + decrementIcon: 'text-sm', + increment: 'p-1.25', + incrementIcon: 'text-sm', + input: 'h-7 text-sm' + }, + md: { + decrement: 'p-1.25', + decrementIcon: 'text-sm', + increment: 'p-1.25', + incrementIcon: 'text-sm', + input: 'h-8 text-sm' + }, + lg: { + decrement: 'p-1.5', + decrementIcon: 'text-base', + increment: 'p-1.5', + incrementIcon: 'text-base', + input: 'h-9 text-base' + }, + xl: { + decrement: 'p-1.5', + decrementIcon: 'text-lg', + increment: 'p-1.5', + incrementIcon: 'text-lg', + input: 'h-10 text-base' + }, + xxl: { + decrement: 'p-2', + decrementIcon: 'text-xl', + increment: 'p-2', + incrementIcon: 'text-xl', + input: 'h-12 text-lg' + } + }, + center: { + true: { + decrement: 'order-1', + input: 'text-center order-2', + increment: 'order-3' + } + } + }, + compoundVariants: [ + { + size: 'xs', + center: false, + class: { + input: 'pl-1.5' + } + }, + { + size: 'sm', + center: false, + class: { + input: 'pl-2' + } + }, + { + size: 'md', + center: false, + class: { + input: 'pl-2.5' + } + }, + { + size: 'lg', + center: false, + class: { + input: 'pl-3' + } + }, + { + size: 'xl', + center: false, + class: { + input: 'pl-3.5' + } + }, + { + size: 'xxl', + center: false, + class: { + input: 'pl-4' + } + } + ], + defaultVariants: { + size: 'md', + center: false + } +}); + +export type NumberFieldSlots = keyof typeof numberFieldVariants.slots; diff --git a/scripts/generate.ts b/scripts/generate.ts index 3da021af..efd8ccf9 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -1,15 +1,28 @@ import fs from 'node:fs'; import fg from 'fast-glob'; -function getComponentFiles(dir: string, module: string) { +function getComponentFiles(module: string) { + const kbName = toKebabCase(module); + const dir = `packages/ui/src/components/${kbName}`; + const files = fg.sync(`**/*.vue`, { onlyFiles: true, cwd: dir }); - files.forEach(file => { - const componentName = file.replace('.vue', ''); - generateComponent(toKebabCase(componentName), module, dir); - }); -} -getComponentFiles('packages/ui/src/components/sidebar', 'sidebar'); + const componentNames: string[] = []; + + files + .filter(file => file.includes('-')) + .forEach(file => { + const componentName = file.replace('.vue', ''); + + componentNames.push(toKebabCase(componentName)); + + generateComponent(toKebabCase(componentName), module, dir); + }); + + generateTypes(componentNames, dir); + generateExports(componentNames, dir); + generateVariants(componentNames, module); +} function toKebabCase(str: string) { return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); @@ -59,3 +72,68 @@ function generateComponent(componentName: string, module: string, dir: string) { fs.writeFileSync(filePath, template); } + +function generateTypes(componentNames: string[], dir: string) { + const filePath = `${dir}/types.ts`; + let template = `import type {\n`; + + componentNames.forEach(componentName => { + template += ` ${toPascalCase(componentName)}Props as _${toPascalCase(componentName)}Props,\n`; + }); + + template += `} from '@soybean-ui/primitives';\n\n`; + + componentNames.forEach(componentName => { + template += `export interface ${toPascalCase(componentName)}Props extends _${toPascalCase(componentName)}Props {}\n\n`; + }); + + fs.writeFileSync(filePath, template); +} + +function generateExports(componentNames: string[], dir: string) { + const filePath = `${dir}/index.ts`; + let template = ``; + + componentNames.forEach(componentName => { + template += `import S${toPascalCase(componentName)} from './${componentName}.vue';\n`; + }); + + template += `\nexport {\n`; + + componentNames.forEach(componentName => { + template += ` S${toPascalCase(componentName)},\n`; + }); + + template += `}\nexport * from './types';`; + + fs.writeFileSync(filePath, template); +} + +function generateVariants(componentNames: string[], module: string) { + const kbName = toKebabCase(module); + const variantsDir = `packages/variants/src/variants/${kbName}.ts`; + let template = `// @unocss-include +import { tv } from 'tailwind-variants'; + +export const ${module}Variants = tv({ + slots: {\n`; + + componentNames.forEach(componentName => { + const slotName = toCamelCase(toCamelCase(componentName).replace(module, '')) || 'root'; + + template += ` ${slotName}: '',\n`; + }); + + template += ` }\n`; + template += `});\n\n`; + + template += `export type ${toPascalCase(module)}Slots = keyof typeof ${module}Variants.slots;`; + + fs.writeFileSync(variantsDir, template); +} + +function start() { + getComponentFiles('numberField'); +} + +start(); diff --git a/src/views/ui/index.vue b/src/views/ui/index.vue index 3fad048d..83e0f628 100644 --- a/src/views/ui/index.vue +++ b/src/views/ui/index.vue @@ -29,6 +29,7 @@ import UiInput from './modules/input.vue'; import UiKeyboardKey from './modules/keyboard-key.vue'; import UiMenubar from './modules/menubar.vue'; import UiNavigationMenu from './modules/navigation-menu.vue'; +import UiNumberField from './modules/number-field.vue'; import UiPagination from './modules/pagination.vue'; import UiPinInput from './modules/pin-input.vue'; import UiPopover from './modules/popover.vue'; @@ -189,6 +190,11 @@ const tabs: TabConfig[] = [ label: 'NavigationMenu', component: UiNavigationMenu }, + { + value: 'number-field', + label: 'NumberField', + component: UiNumberField + }, { value: 'popover', label: 'Popover', diff --git a/src/views/ui/modules/number-field.vue b/src/views/ui/modules/number-field.vue new file mode 100644 index 00000000..4babf8c3 --- /dev/null +++ b/src/views/ui/modules/number-field.vue @@ -0,0 +1,41 @@ + + +