@xelqun/forms 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -2
  3. package/package.json +56 -5
  4. package/resources/css/xelqun.css +162 -0
  5. package/resources/js/components/XqFieldWrapper.vue +59 -0
  6. package/resources/js/components/XqForm.vue +128 -0
  7. package/resources/js/components/XqNode.vue +22 -0
  8. package/resources/js/components/XqUnknownNode.vue +15 -0
  9. package/resources/js/components/defaults.ts +44 -0
  10. package/resources/js/components/fields/XqCheckboxField.vue +48 -0
  11. package/resources/js/components/fields/XqDatePickerField.vue +96 -0
  12. package/resources/js/components/fields/XqFileUploadField.vue +247 -0
  13. package/resources/js/components/fields/XqRadioField.vue +45 -0
  14. package/resources/js/components/fields/XqRepeaterField.vue +166 -0
  15. package/resources/js/components/fields/XqSelectField.vue +151 -0
  16. package/resources/js/components/fields/XqTextInputField.vue +100 -0
  17. package/resources/js/components/fields/XqTextareaField.vue +28 -0
  18. package/resources/js/components/fields/XqToggleField.vue +47 -0
  19. package/resources/js/components/layouts/XqFieldset.vue +16 -0
  20. package/resources/js/components/layouts/XqGrid.vue +13 -0
  21. package/resources/js/components/layouts/XqSection.vue +62 -0
  22. package/resources/js/components/layouts/XqTabsLayout.vue +64 -0
  23. package/resources/js/composables/context.ts +22 -0
  24. package/resources/js/composables/layout.ts +55 -0
  25. package/resources/js/composables/registry.ts +25 -0
  26. package/resources/js/composables/useField.ts +41 -0
  27. package/resources/js/composables/useXqForm.ts +16 -0
  28. package/resources/js/core/conditions.ts +52 -0
  29. package/resources/js/core/expressions.ts +131 -0
  30. package/resources/js/core/http.ts +101 -0
  31. package/resources/js/core/log.ts +93 -0
  32. package/resources/js/core/patch.ts +65 -0
  33. package/resources/js/core/paths.ts +122 -0
  34. package/resources/js/core/queue.ts +190 -0
  35. package/resources/js/core/rows.ts +26 -0
  36. package/resources/js/core/store.ts +560 -0
  37. package/resources/js/core/values.ts +161 -0
  38. package/resources/js/index.ts +24 -0
  39. package/resources/js/lib/utils.ts +18 -0
  40. package/resources/js/plugin.ts +17 -0
  41. package/resources/js/schema/types.ts +99 -0
  42. package/resources/js/ui/XqBadge.vue +33 -0
  43. package/resources/js/ui/XqButton.vue +59 -0
  44. package/resources/js/ui/XqCalendar.vue +90 -0
  45. package/resources/js/ui/XqCard.vue +20 -0
  46. package/resources/js/ui/XqCheckbox.vue +28 -0
  47. package/resources/js/ui/XqCombobox.vue +184 -0
  48. package/resources/js/ui/XqInput.vue +16 -0
  49. package/resources/js/ui/XqLabel.vue +17 -0
  50. package/resources/js/ui/XqRadioGroup.vue +24 -0
  51. package/resources/js/ui/XqRadioGroupItem.vue +31 -0
  52. package/resources/js/ui/XqSelect.vue +79 -0
  53. package/resources/js/ui/XqSwitch.vue +27 -0
  54. package/resources/js/ui/XqTabs.vue +19 -0
  55. package/resources/js/ui/XqTabsContent.vue +17 -0
  56. package/resources/js/ui/XqTabsList.vue +21 -0
  57. package/resources/js/ui/XqTabsTrigger.vue +27 -0
  58. package/resources/js/ui/XqTextarea.vue +16 -0
  59. package/resources/js/ui/classes.ts +12 -0
  60. package/resources/js/ui/index.ts +17 -0
@@ -0,0 +1,24 @@
1
+ export { createXelqunForms } from './plugin';
2
+ export type { XelqunFormsOptions } from './plugin';
3
+
4
+ export { default as XqForm } from './components/XqForm.vue';
5
+ export { default as XqNode } from './components/XqNode.vue';
6
+ export { default as XqFieldWrapper } from './components/XqFieldWrapper.vue';
7
+ export { createRegistry, defaultComponents } from './components/defaults';
8
+
9
+ export { useXqForm } from './composables/useXqForm';
10
+ export { useField } from './composables/useField';
11
+ export { useFormStore } from './composables/context';
12
+ export { gridStyle, spanStyle } from './composables/layout';
13
+ export type { ComponentResolver } from './composables/registry';
14
+
15
+ export { createFormStore, defaultLiveConfig } from './core/store';
16
+ export type { FormStore, FormStoreOptions, SubmitOptions } from './core/store';
17
+ export type { RequestKind, RequestLogEntry } from './core/log';
18
+ export { evaluateCondition } from './core/conditions';
19
+ export { evaluateExpression } from './core/expressions';
20
+ export { getPath, overlaps } from './core/paths';
21
+
22
+ export * from './schema/types';
23
+ export * from './ui';
24
+ export { cn } from './lib/utils';
@@ -0,0 +1,18 @@
1
+ import { clsx } from 'clsx';
2
+ import type { ClassValue } from 'clsx';
3
+ import { twMerge } from 'tailwind-merge';
4
+
5
+ export function cn(...inputs: ClassValue[]): string {
6
+ return twMerge(clsx(inputs));
7
+ }
8
+
9
+ export function formatBytes(bytes: number | null | undefined): string {
10
+ if (!bytes) {
11
+ return '';
12
+ }
13
+
14
+ const units = ['B', 'KB', 'MB', 'GB'];
15
+ const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
16
+
17
+ return `${(bytes / 1024 ** exponent).toFixed(exponent === 0 ? 0 : 1)} ${units[exponent]}`;
18
+ }
@@ -0,0 +1,17 @@
1
+ import type { Plugin } from 'vue';
2
+ import { createRegistry } from './components/defaults';
3
+ import { REGISTRY } from './composables/registry';
4
+ import type { ComponentResolver } from './composables/registry';
5
+
6
+ export interface XelqunFormsOptions {
7
+ /** Extra or replacement renderers keyed by schema type, e.g. { rating: () => import('./Rating.vue') }. */
8
+ components?: Record<string, ComponentResolver>;
9
+ }
10
+
11
+ export function createXelqunForms(options: XelqunFormsOptions = {}): Plugin {
12
+ return {
13
+ install(app) {
14
+ app.provide(REGISTRY, createRegistry(options.components));
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * TypeScript mirror of the JSON schema contract produced by the PHP package.
3
+ * Bump SUPPORTED_SCHEMA_VERSIONS together with Xelqun\Forms\Schema\SchemaVersion.
4
+ */
5
+ export const SUPPORTED_SCHEMA_VERSIONS = [1] as const;
6
+
7
+ export type ConditionNode =
8
+ | { op: 'and' | 'or' | 'not'; args: ConditionNode[] }
9
+ | {
10
+ op: 'eq' | 'neq' | 'in' | 'notIn' | 'gt' | 'gte' | 'lt' | 'lte';
11
+ path: string;
12
+ value: unknown;
13
+ }
14
+ | { op: 'filled' | 'blank'; path: string };
15
+
16
+ export type ExpressionNode =
17
+ | { lit: unknown }
18
+ | { ref: string }
19
+ | { op: string; args: ExpressionNode[] }
20
+ | { fn: string; args: ExpressionNode[] };
21
+
22
+ export type LiveMode = 'change' | 'blur' | 'debounce';
23
+
24
+ export interface LiveConfig {
25
+ mode: LiveMode;
26
+ ms: number | null;
27
+ }
28
+
29
+ export type Breakpoint = 'default' | 'sm' | 'md' | 'lg' | 'xl';
30
+
31
+ export type ColumnSpan = number | 'full' | Partial<Record<Breakpoint, number | 'full'>> | null;
32
+
33
+ export interface SchemaNode {
34
+ key: string;
35
+ type: string;
36
+ hidden: boolean;
37
+ path?: string | null;
38
+ label?: string | null;
39
+ hint?: string | null;
40
+ placeholder?: string | null;
41
+ required?: boolean;
42
+ disabled?: boolean;
43
+ rules?: {
44
+ visible?: ConditionNode | null;
45
+ required?: ConditionNode | null;
46
+ disabled?: ConditionNode | null;
47
+ };
48
+ computed?: { src: string; ast: ExpressionNode } | null;
49
+ live?: LiveConfig | null;
50
+ span?: ColumnSpan;
51
+ props?: Record<string, any>;
52
+ children?: SchemaNode[];
53
+ deps?: string[];
54
+ }
55
+
56
+ export interface FormEndpoints {
57
+ update: string;
58
+ upload: string;
59
+ search: string;
60
+ }
61
+
62
+ export interface FormSchema {
63
+ $xq: number;
64
+ pkg: string;
65
+ id: string;
66
+ token: string | null;
67
+ endpoints: FormEndpoints;
68
+ state: { data: Record<string, any> };
69
+ components: SchemaNode[];
70
+ }
71
+
72
+ export interface UpdateRequest {
73
+ token: string;
74
+ seq: number;
75
+ state: { data: Record<string, any> };
76
+ changed: string[];
77
+ deps: Record<string, string[]>;
78
+ }
79
+
80
+ export interface UpdatePatch {
81
+ $xq: number;
82
+ seq: number;
83
+ set: Record<string, unknown>;
84
+ schema: Record<string, Partial<SchemaNode>>;
85
+ errors: Record<string, string | string[]>;
86
+ }
87
+
88
+ export interface SelectOption {
89
+ value: string | number;
90
+ label: string;
91
+ description?: string | null;
92
+ }
93
+
94
+ export interface UploadResult {
95
+ token: string;
96
+ name: string;
97
+ size: number;
98
+ mime: string | null;
99
+ }
@@ -0,0 +1,33 @@
1
+ <script setup lang="ts">
2
+ import { cva } from 'class-variance-authority';
3
+ import type { VariantProps } from 'class-variance-authority';
4
+ import type { HTMLAttributes } from 'vue';
5
+ import { cn } from '../lib/utils';
6
+
7
+ const badgeVariants = cva(
8
+ 'gap-1 px-2 py-0.5 text-xs font-medium [&>svg]:size-3 inline-flex w-fit shrink-0 items-center rounded-full border whitespace-nowrap',
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: 'border-transparent bg-xq-primary text-xq-primary-foreground',
13
+ secondary: 'border-transparent bg-xq-secondary text-xq-secondary-foreground',
14
+ outline: 'border-xq-border text-xq-foreground',
15
+ success: 'border-transparent bg-xq-success/15 text-xq-success',
16
+ destructive: 'border-transparent bg-xq-destructive/15 text-xq-destructive',
17
+ },
18
+ },
19
+ defaultVariants: { variant: 'default' },
20
+ },
21
+ );
22
+
23
+ const props = defineProps<{
24
+ variant?: VariantProps<typeof badgeVariants>['variant'];
25
+ class?: HTMLAttributes['class'];
26
+ }>();
27
+ </script>
28
+
29
+ <template>
30
+ <span data-slot="xq-badge" :class="cn(badgeVariants({ variant }), props.class)">
31
+ <slot />
32
+ </span>
33
+ </template>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ import { cva } from 'class-variance-authority';
3
+ import type { VariantProps } from 'class-variance-authority';
4
+ import { Primitive } from 'reka-ui';
5
+ import type { PrimitiveProps } from 'reka-ui';
6
+ import type { HTMLAttributes } from 'vue';
7
+ import { cn } from '../lib/utils';
8
+
9
+ const buttonVariants = cva(
10
+ 'gap-2 text-sm font-medium [&_svg:not([class*=size-])]:size-4 inline-flex shrink-0 cursor-pointer items-center justify-center rounded-xq-md whitespace-nowrap transition-[color,background-color,box-shadow,opacity] outline-none focus-visible:ring-[3px] focus-visible:ring-xq-ring/40 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0',
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default:
15
+ 'shadow-xs bg-xq-primary text-xq-primary-foreground hover:bg-xq-primary/90',
16
+ secondary: 'bg-xq-secondary text-xq-secondary-foreground hover:bg-xq-secondary/80',
17
+ outline:
18
+ 'shadow-xs border border-xq-border bg-xq-background text-xq-foreground hover:bg-xq-accent hover:text-xq-accent-foreground',
19
+ ghost: 'text-xq-foreground hover:bg-xq-accent hover:text-xq-accent-foreground',
20
+ destructive:
21
+ 'shadow-xs bg-xq-destructive text-xq-destructive-foreground hover:bg-xq-destructive/90',
22
+ link: 'text-xq-primary underline-offset-4 hover:underline',
23
+ },
24
+ size: {
25
+ default: 'h-9 px-4 py-2',
26
+ sm: 'h-8 gap-1.5 px-3 text-xs',
27
+ lg: 'h-10 px-6',
28
+ icon: 'size-9',
29
+ 'icon-sm': 'size-7',
30
+ },
31
+ },
32
+ defaultVariants: { variant: 'default', size: 'default' },
33
+ },
34
+ );
35
+
36
+ type ButtonVariants = VariantProps<typeof buttonVariants>;
37
+
38
+ const props = withDefaults(
39
+ defineProps<
40
+ PrimitiveProps & {
41
+ variant?: ButtonVariants['variant'];
42
+ size?: ButtonVariants['size'];
43
+ class?: HTMLAttributes['class'];
44
+ }
45
+ >(),
46
+ { as: 'button' },
47
+ );
48
+ </script>
49
+
50
+ <template>
51
+ <Primitive
52
+ data-slot="xq-button"
53
+ :as="as"
54
+ :as-child="asChild"
55
+ :class="cn(buttonVariants({ variant, size }), props.class)"
56
+ >
57
+ <slot />
58
+ </Primitive>
59
+ </template>
@@ -0,0 +1,90 @@
1
+ <script setup lang="ts">
2
+ import type { DateValue } from '@internationalized/date';
3
+ import { ChevronLeft, ChevronRight } from 'lucide-vue-next';
4
+ import {
5
+ CalendarCell,
6
+ CalendarCellTrigger,
7
+ CalendarGrid,
8
+ CalendarGridBody,
9
+ CalendarGridHead,
10
+ CalendarGridRow,
11
+ CalendarHeadCell,
12
+ CalendarHeader,
13
+ CalendarHeading,
14
+ CalendarNext,
15
+ CalendarPrev,
16
+ CalendarRoot,
17
+ } from 'reka-ui';
18
+ import type { HTMLAttributes } from 'vue';
19
+ import { cn } from '../lib/utils';
20
+
21
+ const props = defineProps<{
22
+ minValue?: DateValue;
23
+ maxValue?: DateValue;
24
+ placeholder?: DateValue;
25
+ class?: HTMLAttributes['class'];
26
+ }>();
27
+ const model = defineModel<DateValue | undefined>();
28
+
29
+ const navClass =
30
+ 'inline-flex size-7 cursor-pointer items-center justify-center rounded-xq-md border border-xq-border bg-transparent opacity-70 hover:bg-xq-accent hover:opacity-100 disabled:pointer-events-none disabled:opacity-30';
31
+ </script>
32
+
33
+ <template>
34
+ <CalendarRoot
35
+ v-slot="{ grid, weekDays }"
36
+ v-model="model as any"
37
+ :min-value="minValue"
38
+ :max-value="maxValue"
39
+ :placeholder="placeholder"
40
+ fixed-weeks
41
+ weekday-format="short"
42
+ data-slot="xq-calendar"
43
+ :class="cn('p-3', props.class)"
44
+ >
45
+ <CalendarHeader class="relative flex w-full items-center justify-between">
46
+ <CalendarPrev :class="navClass"><ChevronLeft class="size-4" /></CalendarPrev>
47
+ <CalendarHeading class="text-sm font-medium" />
48
+ <CalendarNext :class="navClass"><ChevronRight class="size-4" /></CalendarNext>
49
+ </CalendarHeader>
50
+ <div class="mt-3 gap-y-4 flex flex-col">
51
+ <CalendarGrid
52
+ v-for="month in grid"
53
+ :key="month.value.toString()"
54
+ class="w-full border-collapse"
55
+ >
56
+ <CalendarGridHead>
57
+ <CalendarGridRow class="flex">
58
+ <CalendarHeadCell
59
+ v-for="day in weekDays"
60
+ :key="day"
61
+ class="w-8 font-normal text-[0.75rem] text-xq-muted-foreground"
62
+ >
63
+ {{ day.slice(0, 2) }}
64
+ </CalendarHeadCell>
65
+ </CalendarGridRow>
66
+ </CalendarGridHead>
67
+ <CalendarGridBody>
68
+ <CalendarGridRow
69
+ v-for="(weekDates, index) in month.rows"
70
+ :key="index"
71
+ class="mt-1 flex w-full"
72
+ >
73
+ <CalendarCell
74
+ v-for="weekDate in weekDates"
75
+ :key="weekDate.toString()"
76
+ :date="weekDate"
77
+ class="p-0 text-sm relative text-center"
78
+ >
79
+ <CalendarCellTrigger
80
+ :day="weekDate"
81
+ :month="month.value"
82
+ class="size-8 text-sm font-normal data-[today]:font-semibold inline-flex cursor-pointer items-center justify-center rounded-xq-md outline-none hover:bg-xq-accent focus-visible:ring-[3px] focus-visible:ring-xq-ring/30 data-[disabled]:pointer-events-none data-[disabled]:opacity-30 data-[outside-view]:text-xq-muted-foreground/60 data-[selected]:bg-xq-primary data-[selected]:text-xq-primary-foreground data-[today]:text-xq-primary data-[selected]:data-[today]:text-xq-primary-foreground data-[unavailable]:line-through"
83
+ />
84
+ </CalendarCell>
85
+ </CalendarGridRow>
86
+ </CalendarGridBody>
87
+ </CalendarGrid>
88
+ </div>
89
+ </CalendarRoot>
90
+ </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue';
3
+ import { cn } from '../lib/utils';
4
+
5
+ const props = defineProps<{ class?: HTMLAttributes['class'] }>();
6
+ </script>
7
+
8
+ <template>
9
+ <div
10
+ data-slot="xq-card"
11
+ :class="
12
+ cn(
13
+ 'shadow-xs rounded-xq-lg border border-xq-border bg-xq-card text-xq-card-foreground',
14
+ props.class,
15
+ )
16
+ "
17
+ >
18
+ <slot />
19
+ </div>
20
+ </template>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ import { Check } from 'lucide-vue-next';
3
+ import { CheckboxIndicator, CheckboxRoot } from 'reka-ui';
4
+ import type { HTMLAttributes } from 'vue';
5
+ import { cn } from '../lib/utils';
6
+
7
+ const props = defineProps<{ id?: string; disabled?: boolean; class?: HTMLAttributes['class'] }>();
8
+ const model = defineModel<boolean>({ default: false });
9
+ </script>
10
+
11
+ <template>
12
+ <CheckboxRoot
13
+ :id="id"
14
+ v-model="model"
15
+ data-slot="xq-checkbox"
16
+ :disabled="disabled"
17
+ :class="
18
+ cn(
19
+ 'peer size-4 shadow-xs shrink-0 cursor-pointer rounded-[4px] border border-xq-input bg-xq-background transition-shadow outline-none focus-visible:ring-[3px] focus-visible:ring-xq-ring/30 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-xq-destructive data-[state=checked]:border-xq-primary data-[state=checked]:bg-xq-primary data-[state=checked]:text-xq-primary-foreground',
20
+ props.class,
21
+ )
22
+ "
23
+ >
24
+ <CheckboxIndicator class="flex items-center justify-center text-current">
25
+ <Check class="size-3.5" :stroke-width="3" />
26
+ </CheckboxIndicator>
27
+ </CheckboxRoot>
28
+ </template>
@@ -0,0 +1,184 @@
1
+ <script setup lang="ts">
2
+ import { Check, ChevronsUpDown, LoaderCircle, X } from 'lucide-vue-next';
3
+ import {
4
+ ComboboxAnchor,
5
+ ComboboxContent,
6
+ ComboboxEmpty,
7
+ ComboboxInput,
8
+ ComboboxItem,
9
+ ComboboxItemIndicator,
10
+ ComboboxPortal,
11
+ ComboboxRoot,
12
+ ComboboxTrigger,
13
+ ComboboxViewport,
14
+ } from 'reka-ui';
15
+ import { computed, reactive, ref, watch } from 'vue';
16
+ import type { HTMLAttributes } from 'vue';
17
+ import { cn } from '../lib/utils';
18
+ import type { SelectOption } from '../schema/types';
19
+ import { controlClass, optionClass, popoverClass } from './classes';
20
+
21
+ type Value = string | number;
22
+
23
+ const props = defineProps<{
24
+ id?: string;
25
+ options: SelectOption[];
26
+ multiple?: boolean;
27
+ remote?: boolean;
28
+ loading?: boolean;
29
+ placeholder?: string | null;
30
+ disabled?: boolean;
31
+ invalid?: boolean;
32
+ describedBy?: string;
33
+ searchFailed?: boolean;
34
+ class?: HTMLAttributes['class'];
35
+ }>();
36
+ const emit = defineEmits<{ search: [term: string]; blur: [] }>();
37
+ const model = defineModel<Value | Value[] | null>();
38
+ const searchTerm = ref('');
39
+ const labels = reactive(new Map<string, string>());
40
+
41
+ watch(
42
+ () => props.options,
43
+ (options) => options.forEach((option) => labels.set(String(option.value), option.label)),
44
+ { immediate: true, deep: true },
45
+ );
46
+
47
+ const selectedValues = computed<Value[]>(() => {
48
+ if (Array.isArray(model.value)) {
49
+ return model.value;
50
+ }
51
+
52
+ return model.value === null || model.value === undefined || model.value === ''
53
+ ? []
54
+ : [model.value];
55
+ });
56
+
57
+ function labelFor(value: unknown): string {
58
+ if (value === null || value === undefined || value === '') {
59
+ return '';
60
+ }
61
+
62
+ return labels.get(String(value)) ?? String(value);
63
+ }
64
+
65
+ function remove(value: Value): void {
66
+ model.value = selectedValues.value.filter((item) => item !== value);
67
+ }
68
+
69
+ function clear(): void {
70
+ model.value = props.multiple ? [] : null;
71
+ searchTerm.value = '';
72
+ }
73
+ </script>
74
+
75
+ <template>
76
+ <ComboboxRoot
77
+ v-model="model as any"
78
+ :multiple="multiple"
79
+ :disabled="disabled"
80
+ :ignore-filter="remote"
81
+ open-on-click
82
+ :reset-search-term-on-blur="!multiple"
83
+ @update:open="(open) => !open && emit('blur')"
84
+ >
85
+ <ComboboxAnchor
86
+ data-slot="xq-combobox"
87
+ :class="
88
+ cn(
89
+ controlClass,
90
+ 'min-h-9 gap-1 py-0.5 pr-1.5 pl-2 flex flex-wrap items-center has-[input:focus-visible]:border-xq-ring has-[input:focus-visible]:ring-[3px] has-[input:focus-visible]:ring-xq-ring/25',
91
+ invalid && 'border-xq-destructive',
92
+ props.class,
93
+ )
94
+ "
95
+ >
96
+ <template v-if="multiple">
97
+ <span
98
+ v-for="value in selectedValues"
99
+ :key="String(value)"
100
+ class="gap-1 py-0.5 pr-1 pl-2 text-xs font-medium inline-flex items-center rounded-xq-sm bg-xq-secondary text-xq-secondary-foreground"
101
+ >
102
+ {{ labelFor(value) }}
103
+ <button
104
+ type="button"
105
+ class="rounded-sm cursor-pointer opacity-60 hover:opacity-100"
106
+ :aria-label="`Remove ${labelFor(value)}`"
107
+ :disabled="disabled"
108
+ @click.stop="remove(value)"
109
+ >
110
+ <X class="size-3" />
111
+ </button>
112
+ </span>
113
+ </template>
114
+ <ComboboxInput
115
+ :id="id"
116
+ v-model="searchTerm"
117
+ @input="emit('search', ($event.target as HTMLInputElement).value)"
118
+ :display-value="multiple ? undefined : labelFor"
119
+ :placeholder="selectedValues.length && multiple ? '' : (placeholder ?? 'Search…')"
120
+ :aria-invalid="invalid || undefined"
121
+ :aria-describedby="describedBy"
122
+ class="h-7 min-w-16 px-1 text-sm flex-1 bg-transparent outline-none placeholder:text-xq-muted-foreground disabled:cursor-not-allowed"
123
+ />
124
+ <LoaderCircle
125
+ v-if="loading"
126
+ class="size-4 motion-safe:animate-spin shrink-0 text-xq-muted-foreground"
127
+ />
128
+ <button
129
+ v-else-if="selectedValues.length && !disabled"
130
+ type="button"
131
+ class="size-6 rounded-sm flex cursor-pointer items-center justify-center text-xq-muted-foreground hover:text-xq-foreground"
132
+ aria-label="Clear selection"
133
+ @click.stop="clear"
134
+ >
135
+ <X class="size-3.5" />
136
+ </button>
137
+ <ComboboxTrigger
138
+ class="size-6 flex cursor-pointer items-center justify-center text-xq-muted-foreground"
139
+ >
140
+ <ChevronsUpDown class="size-4" />
141
+ </ComboboxTrigger>
142
+ </ComboboxAnchor>
143
+ <ComboboxPortal>
144
+ <ComboboxContent
145
+ position="popper"
146
+ :side-offset="4"
147
+ :class="cn(popoverClass, 'max-h-72 min-w-40 w-(--reka-combobox-trigger-width)')"
148
+ >
149
+ <ComboboxViewport class="p-1">
150
+ <p
151
+ v-if="searchFailed"
152
+ role="status"
153
+ class="px-2 py-1.5 text-xs text-xq-destructive"
154
+ >
155
+ The search failed. Type again to retry.
156
+ </p>
157
+ <ComboboxEmpty class="px-2 py-6 text-sm text-center text-xq-muted-foreground">
158
+ {{
159
+ loading
160
+ ? 'Searching…'
161
+ : searchTerm || !remote
162
+ ? 'No results'
163
+ : 'Type to search'
164
+ }}
165
+ </ComboboxEmpty>
166
+ <ComboboxItem
167
+ v-for="option in options"
168
+ :key="String(option.value)"
169
+ :value="option.value"
170
+ :text-value="option.label"
171
+ :class="optionClass"
172
+ >
173
+ <span class="truncate">{{ option.label }}</span>
174
+ <ComboboxItemIndicator
175
+ class="right-2 size-3.5 absolute flex items-center justify-center"
176
+ >
177
+ <Check class="size-4" />
178
+ </ComboboxItemIndicator>
179
+ </ComboboxItem>
180
+ </ComboboxViewport>
181
+ </ComboboxContent>
182
+ </ComboboxPortal>
183
+ </ComboboxRoot>
184
+ </template>
@@ -0,0 +1,16 @@
1
+ <script setup lang="ts">
2
+ import type { HTMLAttributes } from 'vue';
3
+ import { cn } from '../lib/utils';
4
+ import { controlClass } from './classes';
5
+
6
+ const props = defineProps<{ class?: HTMLAttributes['class'] }>();
7
+ const model = defineModel<string | number | null>();
8
+ </script>
9
+
10
+ <template>
11
+ <input
12
+ v-model="model"
13
+ data-slot="xq-input"
14
+ :class="cn(controlClass, 'h-9 px-3 py-1', props.class)"
15
+ />
16
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ import { Label } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { cn } from '../lib/utils';
5
+
6
+ const props = defineProps<{ for?: string; class?: HTMLAttributes['class'] }>();
7
+ </script>
8
+
9
+ <template>
10
+ <Label
11
+ data-slot="xq-label"
12
+ :for="props.for"
13
+ :class="cn('text-sm font-medium leading-none text-xq-foreground select-none', props.class)"
14
+ >
15
+ <slot />
16
+ </Label>
17
+ </template>
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ import { RadioGroupRoot } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { cn } from '../lib/utils';
5
+
6
+ const props = defineProps<{
7
+ disabled?: boolean;
8
+ orientation?: 'horizontal' | 'vertical';
9
+ class?: HTMLAttributes['class'];
10
+ }>();
11
+ const model = defineModel<string | number | null>();
12
+ </script>
13
+
14
+ <template>
15
+ <RadioGroupRoot
16
+ v-model="model"
17
+ data-slot="xq-radio-group"
18
+ :disabled="disabled"
19
+ :orientation="orientation"
20
+ :class="cn('gap-2.5 grid', props.class)"
21
+ >
22
+ <slot />
23
+ </RadioGroupRoot>
24
+ </template>
@@ -0,0 +1,31 @@
1
+ <script setup lang="ts">
2
+ import { RadioGroupIndicator, RadioGroupItem } from 'reka-ui';
3
+ import type { HTMLAttributes } from 'vue';
4
+ import { cn } from '../lib/utils';
5
+
6
+ const props = defineProps<{
7
+ id?: string;
8
+ value: string | number;
9
+ disabled?: boolean;
10
+ class?: HTMLAttributes['class'];
11
+ }>();
12
+ </script>
13
+
14
+ <template>
15
+ <RadioGroupItem
16
+ :id="id"
17
+ data-slot="xq-radio-group-item"
18
+ :value="value"
19
+ :disabled="disabled"
20
+ :class="
21
+ cn(
22
+ 'size-4 shadow-xs aspect-square shrink-0 cursor-pointer rounded-full border border-xq-input bg-xq-background transition-shadow outline-none focus-visible:ring-[3px] focus-visible:ring-xq-ring/30 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-xq-primary',
23
+ props.class,
24
+ )
25
+ "
26
+ >
27
+ <RadioGroupIndicator class="flex items-center justify-center">
28
+ <span class="size-2 rounded-full bg-xq-primary" />
29
+ </RadioGroupIndicator>
30
+ </RadioGroupItem>
31
+ </template>