@brickflow/ui 0.0.53 → 0.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  },
5
5
  "configKey": "brickflowUi",
6
6
  "name": "@brickflow/ui",
7
- "version": "0.0.53",
7
+ "version": "0.0.54",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.3",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -265,7 +265,6 @@ const collectUiStylePathsFromCode = (code) => {
265
265
  }
266
266
  return paths.filter((path) => path.length > 0);
267
267
  };
268
- const collectUiStyleConfigPathsFromCode = (code, id) => collectUiStylePathsFromCode(code).map((path) => resolveUiStyleConfigPath(id, path));
269
268
  const collectUiConfigValuePaths = (value, prefix) => {
270
269
  if (typeof value === "string" || isBrickflowUiStyleReference(value)) {
271
270
  return [prefix];
@@ -345,18 +344,31 @@ const collectUiConfigSchemaEntriesFromCode = (code, id) => {
345
344
  }
346
345
  return entries;
347
346
  };
348
- const collectUiConfigStyleReferencePathsFromValue = (value, componentPath) => {
349
- if (isBrickflowUiStyleReference(value)) {
350
- return [[...componentPath, ...getBrickflowUiStyleReferencePath(value)]];
351
- }
352
- if (typeof value === "string") {
353
- return [];
347
+ const collectUiStylePathsWithConfig = (code, id, config) => {
348
+ const [componentName] = resolveUiStyleConfigPath(id, []);
349
+ const componentConfig = componentName ? config[componentName] : void 0;
350
+ const configReferences = [];
351
+ const addConfigReferences = (value, configPath = []) => {
352
+ if (isBrickflowUiStyleReference(value)) {
353
+ const referencePath = getBrickflowUiStyleReferencePath(value);
354
+ configReferences.push(`UI_STYLE.${referencePath.join(".")}`);
355
+ if (configPath.length >= 2 && referencePath.length >= configPath.length) {
356
+ const prefixLength = referencePath.length - configPath.length + 1;
357
+ const requiredPath = [...referencePath.slice(0, prefixLength), ...configPath.slice(1)];
358
+ configReferences.push(`UI_STYLE.${requiredPath.join(".")}`);
359
+ }
360
+ } else if (typeof value !== "string") {
361
+ for (const [key, childValue] of Object.entries(value)) {
362
+ addConfigReferences(childValue, [...configPath, key]);
363
+ }
364
+ }
365
+ };
366
+ if (componentConfig) {
367
+ addConfigReferences(componentConfig);
354
368
  }
355
- return Object.values(value).flatMap(
356
- (childValue) => collectUiConfigStyleReferencePathsFromValue(childValue, componentPath)
357
- );
369
+ return collectUiStylePathsFromCode(`${configReferences.join("\n")}
370
+ ${code}`);
358
371
  };
359
- const collectUiConfigStyleReferencePaths = (config) => Object.entries(config).flatMap(([key, value]) => collectUiConfigStyleReferencePathsFromValue(value, [key]));
360
372
  const renderInterfaceDeclaration = (name, paths, objectType, leafType = "string") => {
361
373
  const tree = createUiStylePathTree();
362
374
  for (const path of paths) {
@@ -575,10 +587,7 @@ const toComponentStyleKey = (path) => {
575
587
  const componentName = basename(dirname(path));
576
588
  return `${componentName[0]?.toLowerCase()}${componentName.slice(1)}`;
577
589
  };
578
- const collectComponentStyleValues = (source, componentPath, config) => [
579
- ...collectUiStylePathsFromCode(source),
580
- ...collectUiConfigStyleReferencePaths(config.uiConfig).filter(([component]) => component === toComponentStyleKey(componentPath)).map((path) => path.slice(1))
581
- ].map((path) => path.join(".")).filter(Boolean).filter((path, index, paths) => paths.indexOf(path) === index).sort().map((path) => {
590
+ const collectComponentStyleValues = (source, componentPath, config) => collectUiStylePathsWithConfig(source, componentPath, config.uiConfig).map((path) => path.join(".")).filter(Boolean).filter((path, index, paths) => paths.indexOf(path) === index).sort().map((path) => {
582
591
  const segments = path.split(".");
583
592
  const componentValue = readObjectPath(config.uiStyles, [toComponentStyleKey(componentPath), ...segments]);
584
593
  const globalValue = readObjectPath(config.uiStyles, segments);
@@ -736,15 +745,14 @@ const module$1 = defineNuxtModule({
736
745
  }))
737
746
  );
738
747
  const uiConfig = (await loadUiConfig()).uiConfig;
739
- const uiConfigStyleReferencePaths = collectUiConfigStyleReferencePaths(uiConfig);
740
- const paths = [
741
- ...fileContents.flatMap(({ content }) => collectUiStylePathsFromCode(content)),
742
- ...uiConfigStyleReferencePaths.map((path) => path.slice(1))
743
- ];
744
- const configPaths = fileContents.flatMap(
745
- ({ content, file }) => collectUiStyleConfigPathsFromCode(content, file)
748
+ const stylePaths = fileContents.map(({ content, file }) => ({
749
+ file,
750
+ paths: collectUiStylePathsWithConfig(content, file, uiConfig)
751
+ }));
752
+ const paths = stylePaths.flatMap(({ paths: componentPaths }) => componentPaths);
753
+ const configPaths = stylePaths.flatMap(
754
+ ({ file, paths: componentPaths }) => componentPaths.map((path) => resolveUiStyleConfigPath(file, path))
746
755
  );
747
- configPaths.push(...uiConfigStyleReferencePaths);
748
756
  const uiConfigPaths = fileContents.flatMap(
749
757
  ({ content, file }) => collectUiConfigPathsFromCode(content, file, uiConfig)
750
758
  );
@@ -783,15 +791,14 @@ const module$1 = defineNuxtModule({
783
791
  });
784
792
  nuxt.hook("prepare:types", ({ references }) => {
785
793
  references.push({ path: resolver.resolve("./runtime/config.d.ts") });
794
+ references.push({ path: resolver.resolve("./runtime/ui-config.d.ts") });
795
+ references.push({ path: resolver.resolve("./runtime/brickflow-ui.d.ts") });
786
796
  });
787
797
  const iconFontTemplate = addTemplate({
788
798
  filename: "brickflow/brickflow-ui-icons.mjs",
789
799
  getContents: async () => {
790
800
  const iconNames = iconFont ? await getIconFontNames(iconFont.inputDir) : [];
791
801
  return [
792
- "import { uiConfig } from '#brickflow-ui-config'",
793
- "",
794
- "export const buttonConfig = uiConfig.button",
795
802
  `export const iconNames = ${JSON.stringify(iconNames)}`,
796
803
  `export const firstIconName = ${JSON.stringify(iconNames[0] ?? "")}`,
797
804
  ""
@@ -814,6 +821,14 @@ const module$1 = defineNuxtModule({
814
821
  ""
815
822
  ].join("\n")
816
823
  });
824
+ const brickflowUiTemplate = addTemplate({
825
+ filename: "brickflow/brickflow-ui.mjs",
826
+ getContents: () => [
827
+ "export { UI_CONFIG, UI_STYLE, uiConfig, uiStyles } from './brickflow-ui-config.mjs'",
828
+ "export { firstIconName, iconNames } from './brickflow-ui-icons.mjs'",
829
+ ""
830
+ ].join("\n")
831
+ });
817
832
  const uiOptionsTemplate = addTemplate({
818
833
  filename: "brickflow/brickflow-ui-options.mjs",
819
834
  getContents: () => `export const uiThemeEnabled = ${JSON.stringify(options.theme === true)}
@@ -909,6 +924,7 @@ const module$1 = defineNuxtModule({
909
924
  ].join("\n");
910
925
  }
911
926
  });
927
+ nuxt.options.alias["#brickflow-ui"] = brickflowUiTemplate.dst;
912
928
  nuxt.options.alias["#brickflow-ui-config"] = uiConfigTemplate.dst;
913
929
  nuxt.options.alias["#brickflow-ui-catalog"] = uiCatalogTemplate.dst;
914
930
  nuxt.options.alias["#brickflow-ui-icons"] = iconFontTemplate.dst;
@@ -0,0 +1,4 @@
1
+ declare module '#brickflow-ui' {
2
+ export { UI_CONFIG, UI_STYLE, uiConfig, uiStyles } from '#brickflow-ui-config'
3
+ export { firstIconName, iconNames } from '#brickflow-ui-icons'
4
+ }
@@ -6,7 +6,7 @@
6
6
  </script>
7
7
 
8
8
  <script lang="ts" setup>
9
- import { firstIconName } from '#brickflow-ui-icons'
9
+ import { firstIconName } from '#brickflow-ui'
10
10
 
11
11
  import Button from './index.vue'
12
12
  </script>
@@ -21,6 +21,8 @@
21
21
 
22
22
  import Icon from '../Icon/index.vue'
23
23
  import BaseLink from '../Link/index.vue'
24
+ import TransitionSize from '../TransitionSize/index.vue'
25
+
24
26
  const props = withDefaults(
25
27
  defineProps<{
26
28
  block?: boolean
@@ -57,6 +59,8 @@
57
59
  type ButtonSize = Extract<keyof typeof sizeClasses, string>
58
60
  type ButtonVariant = Extract<keyof (typeof colorClasses)[ButtonColor], string>
59
61
 
62
+ const baseRef = shallowRef()
63
+
60
64
  const isButton = computed(() => props.to === undefined)
61
65
  const component = computed(() => (isButton.value ? 'button' : BaseLink))
62
66
  const disabled = computed(() => props.disabled || props.loading)
@@ -82,6 +86,7 @@
82
86
  <template>
83
87
  <component
84
88
  :is="component"
89
+ ref="baseRef"
85
90
  v-bind="animationTapClass ? { onPointerupPassive: handleTap } : {}"
86
91
  :to="props.to"
87
92
  :target="isButton ? undefined : props.target"
@@ -97,7 +102,23 @@
97
102
  }"
98
103
  @contextmenu.prevent
99
104
  >
105
+ <TransitionSize
106
+ v-if="!$slots.leading && !props.icon && !props.trailingIcon"
107
+ dimension="width"
108
+ :flexParent="baseRef?.$el || baseRef"
109
+ flexDirection="start"
110
+ >
111
+ <span v-if="props.loading">
112
+ <Icon
113
+ :name="UI_CONFIG.loadingIconName"
114
+ :class="[UI_STYLE.state.loading, sizeIconClasses[props.size]]"
115
+ aria-hidden="true"
116
+ />
117
+ </span>
118
+ </TransitionSize>
119
+
100
120
  <Transition
121
+ v-if="$slots.leading || props.icon"
101
122
  :name="UI_CONFIG.loadingIconTransition"
102
123
  mode="out-in"
103
124
  >
@@ -109,7 +130,7 @@
109
130
  />
110
131
  </span>
111
132
  <span
112
- v-else-if="$slots.leading || props.icon"
133
+ v-else
113
134
  :class="UI_STYLE.slot.leading"
114
135
  >
115
136
  <slot name="leading">
@@ -129,17 +150,30 @@
129
150
  <slot />
130
151
  </span>
131
152
 
132
- <span
153
+ <Transition
133
154
  v-if="$slots.trailing || props.trailingIcon"
134
- :class="UI_STYLE.slot.trailing"
155
+ :name="UI_CONFIG.loadingIconTransition"
156
+ mode="out-in"
135
157
  >
136
- <slot name="trailing">
158
+ <span v-if="props.loading && !props.icon">
137
159
  <Icon
138
- :name="props.trailingIcon!"
139
- :class="sizeIconClasses[props.size]"
160
+ :name="UI_CONFIG.loadingIconName"
161
+ :class="[UI_STYLE.state.loading, sizeIconClasses[props.size]]"
140
162
  aria-hidden="true"
141
163
  />
142
- </slot>
143
- </span>
164
+ </span>
165
+ <span
166
+ v-else
167
+ :class="UI_STYLE.slot.trailing"
168
+ >
169
+ <slot name="trailing">
170
+ <Icon
171
+ :name="props.trailingIcon!"
172
+ :class="sizeIconClasses[props.size]"
173
+ aria-hidden="true"
174
+ />
175
+ </slot>
176
+ </span>
177
+ </Transition>
144
178
  </component>
145
179
  </template>
@@ -5,9 +5,11 @@
5
5
  </script>
6
6
 
7
7
  <script lang="ts" setup>
8
- import { buttonConfig, firstIconName } from '#brickflow-ui-icons'
8
+ import { firstIconName, uiConfig } from '#brickflow-ui'
9
9
 
10
10
  import Button from './index.vue'
11
+
12
+ const buttonConfig = uiConfig.button
11
13
  </script>
12
14
 
13
15
  <template>
@@ -7,11 +7,12 @@
7
7
  <script lang="ts" setup>
8
8
  import { ref } from 'vue'
9
9
 
10
- import { buttonConfig, firstIconName } from '#brickflow-ui-icons'
10
+ import { firstIconName, uiConfig } from '#brickflow-ui'
11
11
 
12
12
  import Button from './index.vue'
13
13
 
14
- const firstColorClasses = Object.values(buttonConfig.colorClasses)[0] ?? {}
14
+ const buttonConfig = uiConfig.button
15
+ const firstColorClasses = buttonConfig.colorClasses[buttonConfig.colorDefault]
15
16
 
16
17
  const stateActive = ref(true)
17
18
  </script>
@@ -5,11 +5,12 @@
5
5
  </script>
6
6
 
7
7
  <script lang="ts" setup>
8
- import { buttonConfig } from '#brickflow-ui-icons'
8
+ import { uiConfig } from '#brickflow-ui'
9
9
 
10
10
  import Button from './index.vue'
11
11
 
12
- const firstColorClasses = Object.values(buttonConfig.colorClasses)[0] ?? {}
12
+ const buttonConfig = uiConfig.button
13
+ const firstColorClasses = buttonConfig.colorClasses[buttonConfig.colorDefault]
13
14
  </script>
14
15
 
15
16
  <template>
@@ -8,7 +8,7 @@
8
8
  <script lang="ts" setup>
9
9
  import { ref } from 'vue'
10
10
 
11
- import { firstIconName } from '#brickflow-ui-icons'
11
+ import { firstIconName } from '#brickflow-ui'
12
12
 
13
13
  import Icon from './index.vue'
14
14
 
@@ -8,7 +8,7 @@
8
8
  <script lang="ts" setup>
9
9
  import { computed, onBeforeUnmount, ref } from 'vue'
10
10
 
11
- import { iconNames } from '#brickflow-ui-icons'
11
+ import { iconNames } from '#brickflow-ui'
12
12
 
13
13
  import Icon from './index.vue'
14
14
 
@@ -1,10 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
3
 
4
- defineOptions({
5
- inheritAttrs: false,
6
- })
7
-
8
4
  const props = withDefaults(
9
5
  defineProps<{
10
6
  name: string
@@ -17,7 +13,6 @@
17
13
 
18
14
  <template>
19
15
  <i
20
- v-bind="$attrs"
21
16
  :class="classes"
22
17
  role="img"
23
18
  />
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ export const uiDemo = {
3
+ title: 'Alignment',
4
+ }
5
+ </script>
6
+
7
+ <script lang="ts" setup>
8
+ import { capitalize } from 'vue'
9
+
10
+ import Input from './index.vue'
11
+
12
+ const alignments = ['left', 'center', 'right'] as const
13
+ </script>
14
+
15
+ <template>
16
+ <div class="flex max-w-sm flex-col gap-3">
17
+ <Input
18
+ v-for="align in alignments"
19
+ :key="align"
20
+ :align="align"
21
+ :label="`${capitalize(align)} aligned`"
22
+ placeholder="Type here"
23
+ />
24
+ </div>
25
+ </template>
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ export const uiDemo = {
3
+ title: 'Auto Width',
4
+ }
5
+ </script>
6
+
7
+ <script lang="ts" setup>
8
+ import Input from './index.vue'
9
+ </script>
10
+
11
+ <template>
12
+ <div class="flex flex-col gap-3">
13
+ <Input
14
+ placeholder="Type here"
15
+ label="Auto Width"
16
+ autoWidth
17
+ class="max-w-sm"
18
+ />
19
+ </div>
20
+ </template>
@@ -0,0 +1,317 @@
1
+ <script lang="ts">
2
+ const UI_CONFIG = defineUiConfig<{
3
+ hidePasswordIconName: string
4
+ searchClearIconName: string
5
+ showPasswordIconName: string
6
+ sizeClasses: Record<
7
+ string,
8
+ {
9
+ description: string
10
+ field: string
11
+ fieldWrap: string
12
+ icon: string
13
+ label: string
14
+ }
15
+ >
16
+ sizeDefault: string
17
+ variantClasses: Record<
18
+ string,
19
+ {
20
+ description: string
21
+ field: string
22
+ fieldWrap: string
23
+ icon: string
24
+ label: string
25
+ }
26
+ >
27
+ variantDefault: string
28
+ }>()
29
+
30
+ export type InputConfig = typeof UI_CONFIG
31
+ </script>
32
+
33
+ <script setup lang="ts">
34
+ import { twMerge } from 'tailwind-merge'
35
+ import { computed, ref, shallowRef, useAttrs, useId, useSlots, watch } from 'vue'
36
+
37
+ import Icon from '../Icon/index.vue'
38
+
39
+ defineOptions({ inheritAttrs: false })
40
+
41
+ const props = withDefaults(
42
+ defineProps<{
43
+ align?: 'center' | 'left' | 'right'
44
+ autoWidth?: boolean
45
+ description?: string
46
+ disabled?: boolean
47
+ icon?: string
48
+ label?: string
49
+ onBlur?: (value: null | string, target: HTMLInputElement) => void
50
+ onFocus?: (value: null | string, target: HTMLInputElement | null) => void
51
+ onIconClick?: () => void
52
+ onTrailingIconClick?: () => void
53
+ onValueChange?: (value: null | string, target: HTMLInputElement) => void
54
+ onValueInput?: (value: null | string, target: HTMLInputElement | null) => void
55
+ placeholder?: string
56
+ readonly?: boolean
57
+ size?: InputSize
58
+ trailingIcon?: string
59
+ type?: 'decimal' | 'email' | 'numeric' | 'password' | 'search' | 'tel' | 'text' | 'url'
60
+ value?: null | string
61
+ variant?: InputVariant
62
+ }>(),
63
+ {
64
+ align: 'left',
65
+ autoWidth: false,
66
+ description: undefined,
67
+ disabled: false,
68
+ icon: undefined,
69
+ label: undefined,
70
+ onBlur: undefined,
71
+ onFocus: undefined,
72
+ onIconClick: undefined,
73
+ onTrailingIconClick: undefined,
74
+ onValueChange: undefined,
75
+ onValueInput: undefined,
76
+ placeholder: '',
77
+ readonly: false,
78
+ size: 'md',
79
+ trailingIcon: undefined,
80
+ type: 'text',
81
+ value: undefined,
82
+ variant: UI_CONFIG.variantDefault,
83
+ },
84
+ )
85
+
86
+ const model = shallowRef<null | string>('')
87
+ // const model = defineModel<null | number | string>({ default: '' })
88
+
89
+ const attrs = useAttrs()
90
+ const slots = useSlots()
91
+ const generatedId = useId()
92
+ const inputRef = ref<HTMLInputElement | null>(null)
93
+ const widthMeasureRef = ref<HTMLSpanElement | null>(null)
94
+ const measuredWidth = ref(0)
95
+ const hasFocus = ref(false)
96
+ const showPassword = ref(false)
97
+
98
+ const sizeClasses = UI_CONFIG.sizeClasses
99
+ const variantClasses = UI_CONFIG.variantClasses
100
+ type InputSize = Extract<keyof typeof sizeClasses, string>
101
+ type InputVariant = Extract<keyof typeof variantClasses, string>
102
+ const sizeClass = computed(() => sizeClasses[props.size])
103
+ const variantClass = computed(() => variantClasses[props.variant])
104
+
105
+ const fieldId = computed(() => String(attrs.id ?? generatedId))
106
+ const descriptionId = computed(() => `${fieldId.value}-description`)
107
+ const currentValue = computed(() => (props.value === undefined ? model.value : props.value))
108
+ const widthMeasureText = computed(() => currentValue.value || props.placeholder || ' ')
109
+ const isEmptyField = computed(() => {
110
+ return Boolean(currentValue.value)
111
+ })
112
+
113
+ const hasDescription = computed(() => Boolean(props.description || slots.description || slots.error))
114
+ const inputType = computed(() => {
115
+ return props.type === 'password' && !showPassword.value ? 'password' : 'text'
116
+ })
117
+ const classes = computed(() => [
118
+ UI_STYLE.base,
119
+ props.autoWidth && UI_STYLE.state.autoWidth,
120
+ props.disabled && UI_STYLE.state.disabled,
121
+ ])
122
+ const fieldWrapClasses = computed(() => [
123
+ twMerge(UI_STYLE.fieldWrap, sizeClass.value.fieldWrap, variantClass.value.fieldWrap),
124
+ props.readonly && UI_STYLE.state.readonly,
125
+ ])
126
+ const fieldClasses = computed(() => [
127
+ twMerge(UI_STYLE.field, sizeClass.value.field, variantClass.value.field),
128
+ props.align === 'center' && UI_STYLE.align.center,
129
+ props.align === 'right' && UI_STYLE.align.right,
130
+ props.autoWidth && UI_STYLE.state.autoWidthField,
131
+ ])
132
+ const labelClasses = computed(() => twMerge(UI_STYLE.label, sizeClass.value.label, variantClass.value.label))
133
+ const descriptionClasses = computed(() =>
134
+ twMerge(UI_STYLE.description, sizeClass.value.description, variantClass.value.description),
135
+ )
136
+ const leadingIconClasses = computed(() =>
137
+ twMerge(
138
+ UI_STYLE.slot.leading,
139
+ sizeClass.value.icon,
140
+ variantClass.value.icon,
141
+ props.onIconClick && UI_STYLE.slot.hasAction,
142
+ ),
143
+ )
144
+ const trailingIconClasses = computed(() =>
145
+ twMerge(
146
+ UI_STYLE.slot.trailing,
147
+ sizeClass.value.icon,
148
+ variantClass.value.icon,
149
+ props.onTrailingIconClick && UI_STYLE.slot.hasAction,
150
+ ),
151
+ )
152
+ const fieldStyle = computed(() =>
153
+ props.autoWidth ? { flexShrink: 1, maxWidth: '100%', width: `${measuredWidth.value}px` } : { undefined },
154
+ )
155
+
156
+ watch(
157
+ [widthMeasureText, () => props.autoWidth, () => props.size, () => props.variant, widthMeasureRef],
158
+ () => {
159
+ if (props.autoWidth && widthMeasureRef.value) {
160
+ measuredWidth.value = Math.ceil(widthMeasureRef.value.getBoundingClientRect().width) + 2
161
+ }
162
+ },
163
+ { flush: 'post', immediate: true },
164
+ )
165
+
166
+ function clear(): void {
167
+ setValue('')
168
+ focus()
169
+ }
170
+
171
+ function focus(): void {
172
+ inputRef.value?.focus()
173
+ }
174
+
175
+ function handleBlur(event: FocusEvent): void {
176
+ hasFocus.value = false
177
+ props.onBlur?.(currentValue.value, event.target as HTMLInputElement)
178
+ }
179
+
180
+ function handleFocus(event: FocusEvent): void {
181
+ hasFocus.value = true
182
+ props.onFocus?.(currentValue.value, event.target as HTMLInputElement)
183
+ }
184
+
185
+ function onChange(event: Event): void {
186
+ const target = event.target as HTMLInputElement
187
+ props.onValueChange?.(target.value, target)
188
+ }
189
+
190
+ function onInput(event: Event): void {
191
+ setValue((event.target as HTMLInputElement).value)
192
+ }
193
+
194
+ function setValue(value: null | string): void {
195
+ model.value = value
196
+ props.onValueInput?.(value, inputRef.value)
197
+ }
198
+
199
+ defineExpose({ clear, fieldId, focus, hasFocus, inputRef })
200
+ </script>
201
+
202
+ <template>
203
+ <div :class="[classes, $attrs.class]">
204
+ <label
205
+ v-if="props.label || $slots.label"
206
+ :for="fieldId"
207
+ :class="labelClasses"
208
+ >
209
+ <slot name="label">{{ props.label }}</slot>
210
+ </label>
211
+
212
+ <label
213
+ :for="fieldId"
214
+ :class="fieldWrapClasses"
215
+ :style="props.autoWidth ? { maxWidth: '100%', width: 'fit-content' } : undefined"
216
+ >
217
+ <span
218
+ v-if="props.autoWidth"
219
+ ref="widthMeasureRef"
220
+ aria-hidden="true"
221
+ :class="[fieldClasses, 'pointer-events-none invisible absolute w-max whitespace-pre']"
222
+ >
223
+ {{ widthMeasureText }}
224
+ </span>
225
+ <slot name="leading">
226
+ <component
227
+ :is="props.onIconClick ? 'button' : 'label'"
228
+ v-if="props.icon"
229
+ type="button"
230
+ :for="fieldId"
231
+ :disabled="props.disabled || !props.onIconClick"
232
+ :class="leadingIconClasses"
233
+ @click="props.onIconClick?.()"
234
+ >
235
+ <Icon
236
+ :name="props.icon"
237
+ aria-hidden="true"
238
+ />
239
+ </component>
240
+ </slot>
241
+
242
+ <input
243
+ v-bind="{ ...$attrs, class: undefined, id: undefined }"
244
+ :id="fieldId"
245
+ ref="inputRef"
246
+ :value="currentValue"
247
+ :type="inputType"
248
+ :inputmode="props.type === 'password' ? undefined : props.type"
249
+ :disabled="props.disabled"
250
+ :readonly="props.readonly"
251
+ :placeholder="props.placeholder"
252
+ :aria-describedby="hasDescription ? descriptionId : undefined"
253
+ :class="fieldClasses"
254
+ :style="fieldStyle"
255
+ @input="onInput"
256
+ @change="onChange"
257
+ @focus="handleFocus"
258
+ @blur="handleBlur"
259
+ />
260
+
261
+ <button
262
+ v-if="props.type === 'password'"
263
+ type="button"
264
+ :disabled="props.disabled"
265
+ :class="[trailingIconClasses, UI_STYLE.slot.hasAction]"
266
+ :aria-label="showPassword ? 'Hide password' : 'Show password'"
267
+ @click="showPassword = !showPassword"
268
+ >
269
+ <Icon
270
+ :name="showPassword ? UI_CONFIG.hidePasswordIconName : UI_CONFIG.showPasswordIconName"
271
+ aria-hidden="true"
272
+ />
273
+ </button>
274
+ <button
275
+ v-else-if="props.type === 'search' && isEmptyField"
276
+ type="button"
277
+ :disabled="props.disabled || props.readonly"
278
+ :class="[trailingIconClasses, UI_STYLE.slot.hasAction]"
279
+ aria-label="Clear search"
280
+ @click="clear"
281
+ >
282
+ <Icon
283
+ :name="UI_CONFIG.searchClearIconName"
284
+ aria-hidden="true"
285
+ />
286
+ </button>
287
+ <slot
288
+ v-else
289
+ name="trailing"
290
+ >
291
+ <component
292
+ :is="props.onTrailingIconClick ? 'button' : 'label'"
293
+ v-if="props.trailingIcon"
294
+ type="button"
295
+ :for="fieldId"
296
+ :disabled="props.disabled || !props.onTrailingIconClick"
297
+ :class="trailingIconClasses"
298
+ @click="props.onTrailingIconClick?.()"
299
+ >
300
+ <Icon
301
+ :name="props.trailingIcon"
302
+ aria-hidden="true"
303
+ />
304
+ </component>
305
+ </slot>
306
+ </label>
307
+
308
+ <div
309
+ v-if="hasDescription"
310
+ :id="descriptionId"
311
+ :class="descriptionClasses"
312
+ >
313
+ <slot name="error" />
314
+ <slot name="description">{{ props.description }}</slot>
315
+ </div>
316
+ </div>
317
+ </template>
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ export const uiDemo = {
3
+ title: 'Label & Description, Placeholder',
4
+ }
5
+ </script>
6
+
7
+ <script lang="ts" setup>
8
+ import Input from './index.vue'
9
+ </script>
10
+
11
+ <template>
12
+ <div class="max-w-sm">
13
+ <Input
14
+ label="Email address"
15
+ description="We will use this address to contact you."
16
+ placeholder="name@example.com"
17
+ type="email"
18
+ />
19
+ </div>
20
+ </template>
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ export const uiDemo = {
3
+ title: 'Read-only & Slot',
4
+ }
5
+ </script>
6
+
7
+ <script lang="ts" setup>
8
+ import { firstIconName } from '#brickflow-ui'
9
+
10
+ import Input from './index.vue'
11
+ </script>
12
+
13
+ <template>
14
+ <div class="max-w-sm">
15
+ <Input
16
+ label="Read-only value"
17
+ value="You can select this text"
18
+ :trailingIcon="firstIconName"
19
+ readonly
20
+ >
21
+ <template #trailing>
22
+ <div class="h-full bg-red-900 text-red-300">SLOT TEXT</div>
23
+ </template>
24
+ </Input>
25
+ </div>
26
+ </template>
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ export const uiDemo = {
3
+ title: 'Sizes & Icons',
4
+ }
5
+ </script>
6
+
7
+ <script lang="ts" setup>
8
+ import { firstIconName, uiConfig } from '#brickflow-ui'
9
+
10
+ import Button from '../Button/index.vue'
11
+ import Input from './index.vue'
12
+
13
+ const inputConfig = uiConfig.input
14
+
15
+ function copy(): void {
16
+ console.log('Click')
17
+ }
18
+ </script>
19
+
20
+ <template>
21
+ <div class="flex flex-col gap-3">
22
+ <div
23
+ v-for="(_, size) in inputConfig.sizeClasses"
24
+ :key="size"
25
+ class="grid gap-x-2 gap-y-1.5"
26
+ style="grid-template-columns: minmax(0, 24rem) max-content; grid-template-rows: repeat(3, auto)"
27
+ >
28
+ <Input
29
+ :size="size"
30
+ :icon="firstIconName"
31
+ :trailingIcon="firstIconName"
32
+ description="Some text description"
33
+ placeholder="Type here"
34
+ :label="`Size: ${size}`"
35
+ class="row-span-3 grid! grid-rows-subgrid"
36
+ @iconClick="copy"
37
+ />
38
+ <Button
39
+ :size="size"
40
+ class="col-start-2 row-start-2 self-center"
41
+ >
42
+ Button
43
+ </Button>
44
+ </div>
45
+ </div>
46
+ </template>
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ export const uiDemo = {
3
+ title: 'Input Types',
4
+ }
5
+ </script>
6
+
7
+ <script lang="ts" setup>
8
+ import { capitalize } from 'vue'
9
+
10
+ import Input from './index.vue'
11
+
12
+ const types = ['text', 'email', 'decimal', 'numeric', 'password', 'search', 'tel', 'url'] as const
13
+ </script>
14
+
15
+ <template>
16
+ <div class="flex max-w-sm flex-col gap-3">
17
+ <Input
18
+ v-for="type in types"
19
+ :key="type"
20
+ :type="type"
21
+ :label="capitalize(type)"
22
+ :placeholder="`Enter ${type}`"
23
+ />
24
+ </div>
25
+ </template>
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ export const uiDemo = {
3
+ title: 'Variants & Disabled',
4
+ }
5
+ </script>
6
+
7
+ <script lang="ts" setup>
8
+ import { uiConfig } from '#brickflow-ui'
9
+
10
+ import Input from './index.vue'
11
+
12
+ const inputConfig = uiConfig.input
13
+ </script>
14
+
15
+ <template>
16
+ <div class="flex max-w-sm flex-col gap-4">
17
+ <div
18
+ v-for="(_, variant) in inputConfig.variantClasses"
19
+ :key="variant"
20
+ class="flex flex-col gap-3"
21
+ >
22
+ <Input
23
+ :variant="variant"
24
+ :label="`Variant ${variant}`"
25
+ placeholder="Type here"
26
+ />
27
+ <Input
28
+ :variant="variant"
29
+ :label="`Disabled ${variant}`"
30
+ value="Unavailable"
31
+ disabled
32
+ />
33
+ </div>
34
+ </div>
35
+ </template>
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ export const uiDemo = {
3
+ title: 'Number & Bounds',
4
+ }
5
+ </script>
6
+
7
+ <script lang="ts" setup>
8
+ import { ref } from 'vue'
9
+
10
+ import InputNumber from './index.vue'
11
+
12
+ const quantity = ref<null | number>(null)
13
+ const price = ref<null | number>(25)
14
+ </script>
15
+
16
+ <template>
17
+ <div class="flex max-w-sm flex-col gap-4">
18
+ <InputNumber
19
+ v-model="quantity"
20
+ label="Quantity"
21
+ description="Use ↑ and ↓ to change by one."
22
+ :min="0"
23
+ :max="20"
24
+ block
25
+ />
26
+ <InputNumber
27
+ v-model="price"
28
+ label="Price"
29
+ description="Changes in steps of five."
30
+ :min="0"
31
+ :max="100"
32
+ :step="5"
33
+ block
34
+ />
35
+ <p class="text-sm text-gray-400">Quantity: {{ quantity ?? '—' }} · Price: {{ price ?? '—' }}</p>
36
+ </div>
37
+ </template>
@@ -0,0 +1,126 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+
4
+ import Input from '../Input/index.vue'
5
+
6
+ defineOptions({ inheritAttrs: false })
7
+
8
+ const props = withDefaults(
9
+ defineProps<{
10
+ max?: number
11
+ min?: number
12
+ onValueChange?: (value: null | number, target: HTMLInputElement) => void
13
+ onValueInput?: (value: null | number, target: HTMLInputElement | null) => void
14
+ step?: number
15
+ value?: null | number
16
+ }>(),
17
+ {
18
+ max: undefined,
19
+ min: undefined,
20
+ onValueChange: undefined,
21
+ onValueInput: undefined,
22
+ step: 1,
23
+ value: undefined,
24
+ },
25
+ )
26
+
27
+ const model = defineModel<null | number>({ default: null })
28
+ const rawValue = ref<null | string>(null)
29
+ const currentValue = computed(() => (props.value === undefined ? model.value : props.value))
30
+ const inputValue = computed(
31
+ () => rawValue.value ?? (currentValue.value === null ? '' : String(currentValue.value)),
32
+ )
33
+
34
+ function clamp(value: number): number {
35
+ return Math.min(props.max ?? Infinity, Math.max(props.min ?? -Infinity, value))
36
+ }
37
+
38
+ function handleBlur(): void {
39
+ rawValue.value = null
40
+ }
41
+
42
+ function handleKeydown(event: KeyboardEvent): void {
43
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
44
+ return
45
+ }
46
+
47
+ event.preventDefault()
48
+ const next = (currentValue.value ?? 0) + (event.key === 'ArrowUp' ? props.step : -props.step)
49
+ const value = clamp(next)
50
+ rawValue.value = String(value)
51
+ model.value = value
52
+ props.onValueInput?.(value, event.target as HTMLInputElement)
53
+ }
54
+
55
+ function handleValueChange(value: null | string, target: HTMLInputElement): void {
56
+ const parsed = toNumber(value)
57
+ const bounded = parsed === null ? null : clamp(parsed)
58
+ rawValue.value = null
59
+ model.value = bounded
60
+ props.onValueChange?.(bounded, target)
61
+ }
62
+
63
+ function handleValueInput(value: null | string, target: HTMLInputElement | null): void {
64
+ rawValue.value = value ?? ''
65
+ const number = toNumber(value)
66
+ model.value = number
67
+ props.onValueInput?.(number, target)
68
+ }
69
+
70
+ function toNumber(value: null | string): null | number {
71
+ if (value === null || value.trim() === '') {
72
+ return null
73
+ }
74
+
75
+ const number = Number(value.replace(',', '.'))
76
+ return Number.isFinite(number) ? number : null
77
+ }
78
+ </script>
79
+
80
+ <template>
81
+ <Input
82
+ v-bind="{
83
+ ...$attrs,
84
+ onValueInput: handleValueInput,
85
+ onValueChange: handleValueChange,
86
+ onBlur: handleBlur,
87
+ onKeydown: handleKeydown,
88
+ }"
89
+ :value="inputValue"
90
+ type="decimal"
91
+ :min="props.min"
92
+ :max="props.max"
93
+ :step="props.step"
94
+ >
95
+ <template
96
+ v-if="$slots.label"
97
+ #label
98
+ >
99
+ <slot name="label" />
100
+ </template>
101
+ <template
102
+ v-if="$slots.left"
103
+ #left
104
+ >
105
+ <slot name="left" />
106
+ </template>
107
+ <template
108
+ v-if="$slots.right"
109
+ #right
110
+ >
111
+ <slot name="right" />
112
+ </template>
113
+ <template
114
+ v-if="$slots.error"
115
+ #error
116
+ >
117
+ <slot name="error" />
118
+ </template>
119
+ <template
120
+ v-if="$slots.description"
121
+ #description
122
+ >
123
+ <slot name="description" />
124
+ </template>
125
+ </Input>
126
+ </template>
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <Transition
3
+ :name="props.name"
4
+ :mode="props.mode"
5
+ @beforeEnter="beforeStart"
6
+ @enter="start"
7
+ @afterEnter="afterEnter"
8
+ @beforeLeave="beforeLeave"
9
+ @leave="leave"
10
+ @afterLeave="afterLeave"
11
+ >
12
+ <slot></slot>
13
+ </Transition>
14
+ </template>
15
+
16
+ <script lang="ts" setup>
17
+ import { computed, nextTick, ref } from 'vue'
18
+
19
+ const props = withDefaults(
20
+ defineProps<{
21
+ dimension?: 'height' | 'width'
22
+ duration?: number
23
+ expandClasses?: ''
24
+ flexDirection?: 'end' | 'start'
25
+ flexParent?: Element
26
+ mode?: 'default' | 'in-out' | 'out-in'
27
+ name?: string
28
+ }>(),
29
+ {
30
+ dimension: 'height',
31
+ duration: 300,
32
+ expandClasses: '',
33
+ flexDirection: 'start',
34
+ flexParent: undefined,
35
+ gap: 0,
36
+ mode: undefined,
37
+ name: undefined,
38
+ },
39
+ )
40
+
41
+ const gap = ref(0)
42
+
43
+ const sizeName = computed(() => props.dimension)
44
+ const marginName = computed(() => {
45
+ const xDirection = props.flexDirection === 'start' ? 'marginLeft' : 'marginRight'
46
+ const yDirection = props.flexDirection === 'start' ? 'marginTop' : 'marginBottom'
47
+ return props.dimension === 'height' ? yDirection : xDirection
48
+ })
49
+ const scrollName = computed(() => (props.dimension === 'height' ? 'scrollHeight' : 'scrollWidth'))
50
+ const expandClass = computed(() => {
51
+ return (props.expandClasses || '!transition-all !ease-in-out !overflow-hidden will-change-transform').split(
52
+ ' ',
53
+ )
54
+ })
55
+
56
+ const beforeStart = (event: Element): void => {
57
+ const el = event as HTMLElement
58
+ shrink(el)
59
+ }
60
+ const start = (event: Element): void => {
61
+ const el = event as HTMLElement
62
+ el.dataset.oldOverflow = el.style.overflow
63
+
64
+ // Wait until component mounted to get proper height
65
+ nextTick(() => {
66
+ if (el[scrollName.value] === 0) {
67
+ el.style[sizeName.value] = ''
68
+ } else {
69
+ el.style[sizeName.value] = `${el[scrollName.value]}px`
70
+ el.style.padding = ''
71
+ if (props.flexParent) {
72
+ el.style[marginName.value] = ''
73
+ }
74
+ }
75
+ el.style.overflow = 'hidden'
76
+ })
77
+ }
78
+ const afterEnter = (event: Element): void => {
79
+ const el = event as HTMLElement
80
+ release(el)
81
+ }
82
+ const beforeLeave = (event: Element): void => {
83
+ const el = event as HTMLElement
84
+ const computedStyle = getComputedStyle(el)
85
+ el.dataset.oldOverflow = computedStyle.overflow
86
+
87
+ el.style[sizeName.value] = `${el[scrollName.value]}px`
88
+ if (props.flexParent) {
89
+ el.style[marginName.value] = ''
90
+ }
91
+ el.style.overflow = 'hidden'
92
+ }
93
+ const leave = (event: Element): void => {
94
+ const el = event as HTMLElement
95
+ if (el[scrollName.value] !== 0) {
96
+ shrink(el)
97
+ }
98
+ }
99
+ const afterLeave = (event: Element): void => {
100
+ const el = event as HTMLElement
101
+ release(el)
102
+ }
103
+
104
+ function release(el: HTMLElement): void {
105
+ el.classList.add(...expandClass.value)
106
+ el.style.transitionDuration = ''
107
+ el.style[sizeName.value] = ''
108
+ el.style[marginName.value] = ''
109
+ el.style.overflow = String(el.dataset.oldOverflow)
110
+ }
111
+ function shrink(el: HTMLElement): void {
112
+ el.classList.add(...expandClass.value)
113
+ el.style.transitionDuration = `${props.duration}ms`
114
+ el.style[sizeName.value] = '0'
115
+ el.style.padding = '0'
116
+ if (props.flexParent) {
117
+ gap.value = parseInt(window.getComputedStyle(props.flexParent)?.gap) || 0
118
+ el.style[marginName.value] = `-${gap.value}px`
119
+ }
120
+ }
121
+ </script>
@@ -1,18 +1,5 @@
1
1
  export {}
2
2
 
3
- declare module '#brickflow-ui-config' {
4
- import type { BrickflowUiConfig } from './tailwind'
5
-
6
- export const UI_CONFIG: BrickflowUiConfigType
7
- export const UI_STYLE: BrickflowUiStyleType
8
- export const uiConfig: BrickflowUiConfig['uiConfig']
9
- export const uiStyles: BrickflowUiConfig['uiStyles']
10
-
11
- const config: BrickflowUiConfig
12
-
13
- export default config
14
- }
15
-
16
3
  declare global {
17
4
  type BrickflowUiConfigFor<TExpected extends BrickflowUiConfigObject> = {
18
5
  [TKey in keyof BrickflowUiConfigLiteralPaths]: BrickflowUiConfigLiteralPaths[TKey] extends TExpected
@@ -1,5 +1,4 @@
1
1
  declare module '#brickflow-ui-icons' {
2
- export const buttonConfig: import('./components/Button/index.vue').ButtonConfig
3
2
  export const firstIconName: string
4
3
  export const iconNames: readonly string[]
5
4
  }
@@ -0,0 +1,10 @@
1
+ declare module '#brickflow-ui-config' {
2
+ export const UI_CONFIG: BrickflowUiConfigType
3
+ export const UI_STYLE: BrickflowUiStyleType
4
+ export const uiConfig: BrickflowUiConfigLiteralPaths
5
+ export const uiStyles: BrickflowUiStyleType
6
+
7
+ const config: { uiConfig: typeof uiConfig; uiStyles: typeof uiStyles }
8
+
9
+ export default config
10
+ }
@@ -1 +1 @@
1
- @import "./ui-breakpoint.css";@media (width < 1440px){.dkHidden{display:none!important}}@media (width < 1024px),(width >= 1440px){.lpHidden{display:none!important}}@media (width < 768px),(width >= 1024px){.tbHidden{display:none!important}}@media (width < 480px),(width >= 768px){.mbHidden{display:none!important}}@media (width >= 480px){.msHidden{display:none!important}}
1
+ @import "./ui-breakpoint.css";@source "./components";@media (width < 1440px){.dkHidden{display:none!important}}@media (width < 1024px),(width >= 1440px){.lpHidden{display:none!important}}@media (width < 768px),(width >= 1024px){.tbHidden{display:none!important}}@media (width < 480px),(width >= 768px){.mbHidden{display:none!important}}@media (width >= 480px){.msHidden{display:none!important}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brickflow/ui",
3
- "version": "0.0.53",
3
+ "version": "0.0.54",
4
4
  "type": "module",
5
5
  "description": "brickflow UI Nuxt module.",
6
6
  "files": [