@nexxtmove/ui 1.6.0 → 1.8.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.
- package/dist/components/Textarea/Textarea.vue.d.ts +49 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +708 -617
- package/dist/nuxt.js +1 -0
- package/package.json +1 -1
- package/src/components/InputField/InputField.vue +21 -3
- package/src/components/InputSelect/InputSelect.vue +21 -4
- package/src/components/Textarea/Textarea.vue +144 -0
- package/src/components.json +1 -0
- package/src/index.ts +1 -0
package/dist/nuxt.js
CHANGED
|
@@ -51,6 +51,7 @@ var s = {
|
|
|
51
51
|
NexxtTable: "components/Table/Table.vue",
|
|
52
52
|
NexxtTabs: "components/Tabs/Tabs.vue",
|
|
53
53
|
NexxtTemplateCard: "components/TemplateCard/TemplateCard.vue",
|
|
54
|
+
NexxtTextarea: "components/Textarea/Textarea.vue",
|
|
54
55
|
NexxtTimelineEvent: "components/TimelineEvent/TimelineEvent.vue",
|
|
55
56
|
NexxtTimelineLanes: "components/TimelineLanes/TimelineLanes.vue",
|
|
56
57
|
NexxtTimelinePhaseblock: "components/TimelinePhaseblock/TimelinePhaseblock.vue",
|
package/package.json
CHANGED
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, getCurrentInstance, useTemplateRef } from 'vue'
|
|
2
|
+
import { computed, getCurrentInstance, useAttrs, useTemplateRef } from 'vue'
|
|
3
3
|
import Icon from '../Icon/Icon.vue'
|
|
4
4
|
|
|
5
5
|
const INPUT_ID = 'input-field-' + Math.random().toString(36).slice(2, 10)
|
|
6
6
|
|
|
7
|
+
const attrs = useAttrs()
|
|
8
|
+
|
|
9
|
+
// `class` and `style` describe where the field sits in the consumer's layout,
|
|
10
|
+
// so they belong on the outer wrapper. Everything else -- id, name, type,
|
|
11
|
+
// placeholder, data-* , aria-* -- describes the control, so it goes to the
|
|
12
|
+
// input. Nothing lands on both any more.
|
|
13
|
+
const inputAttrs = computed(() => {
|
|
14
|
+
const rest: Record<string, unknown> = { ...attrs }
|
|
15
|
+
delete rest.class
|
|
16
|
+
delete rest.style
|
|
17
|
+
return rest
|
|
18
|
+
})
|
|
19
|
+
|
|
7
20
|
defineOptions({
|
|
8
21
|
name: 'NexxtInputField',
|
|
22
|
+
// Attributes used to land twice: Vue applied them to the root *and* the
|
|
23
|
+
// explicit v-bind put them on the input. `id` was therefore duplicated across
|
|
24
|
+
// two elements, and a consumer's layout class ended up on an input that
|
|
25
|
+
// already carries `w-full` itself. Routing is now explicit: see the template.
|
|
26
|
+
inheritAttrs: false,
|
|
9
27
|
})
|
|
10
28
|
|
|
11
29
|
export interface NexxtInputFieldProps {
|
|
@@ -50,7 +68,7 @@ const decrement = () => {
|
|
|
50
68
|
</script>
|
|
51
69
|
|
|
52
70
|
<template>
|
|
53
|
-
<div class="inline-flex flex-col gap-1">
|
|
71
|
+
<div class="inline-flex flex-col gap-1" :class="attrs.class" :style="attrs.style">
|
|
54
72
|
<label v-if="label" class="justify-start small-normal text-gray-700" :for="INPUT_ID">
|
|
55
73
|
{{ label }}
|
|
56
74
|
<span v-if="required" class="text-brick-500">*</span>
|
|
@@ -69,7 +87,7 @@ const decrement = () => {
|
|
|
69
87
|
:id="INPUT_ID"
|
|
70
88
|
v-model="modelValue"
|
|
71
89
|
:placeholder="placeholder"
|
|
72
|
-
v-bind="
|
|
90
|
+
v-bind="inputAttrs"
|
|
73
91
|
class="bg-transparant h-10 w-full min-w-60 text-sm text-gray-900 placeholder-gray-400 focus:outline-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
74
92
|
:class="{ 'pl-3': !leftIcon, 'pr-3': !rightIcon }"
|
|
75
93
|
/>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
|
-
import { computed } from 'vue'
|
|
2
|
+
import { computed, useAttrs } from 'vue'
|
|
3
3
|
import NexxtIcon from '../Icon/Icon.vue'
|
|
4
4
|
import NexxtFloatingPanel from '../FloatingPanel/FloatingPanel.vue'
|
|
5
5
|
import type Icon from '../Icon/Icon.vue'
|
|
@@ -11,7 +11,24 @@ const generateId = () => 'input-select-' + Math.random().toString(36).slice(2, 1
|
|
|
11
11
|
const fieldId = generateId()
|
|
12
12
|
const selectId = generateId()
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
const attrs = useAttrs()
|
|
15
|
+
|
|
16
|
+
// `class` and `style` position the field in the consumer's layout and belong on
|
|
17
|
+
// the wrapper; everything else describes the control and goes to the input.
|
|
18
|
+
const inputAttrs = computed(() => {
|
|
19
|
+
const rest: Record<string, unknown> = { ...attrs }
|
|
20
|
+
delete rest.class
|
|
21
|
+
delete rest.style
|
|
22
|
+
return rest
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
defineOptions({
|
|
26
|
+
name: 'NexxtInputSelect',
|
|
27
|
+
// Same routing as NexxtInputField: attributes used to land on the root and
|
|
28
|
+
// the input at once, duplicating `id` and putting layout classes on a control
|
|
29
|
+
// that is already full-width. See the template.
|
|
30
|
+
inheritAttrs: false,
|
|
31
|
+
})
|
|
15
32
|
|
|
16
33
|
export interface NexxtInputSelectProps {
|
|
17
34
|
options: SelectOption[]
|
|
@@ -49,7 +66,7 @@ const select = (opt: SelectOption, close: () => void) => {
|
|
|
49
66
|
</script>
|
|
50
67
|
|
|
51
68
|
<template>
|
|
52
|
-
<div class="inline-flex flex-col gap-1">
|
|
69
|
+
<div class="inline-flex flex-col gap-1" :class="attrs.class" :style="attrs.style">
|
|
53
70
|
<label v-if="label" class="justify-start small-normal text-gray-700" :for="fieldId">
|
|
54
71
|
{{ label }}
|
|
55
72
|
<span v-if="required" class="text-brick-500">*</span>
|
|
@@ -114,7 +131,7 @@ const select = (opt: SelectOption, close: () => void) => {
|
|
|
114
131
|
:id="fieldId"
|
|
115
132
|
v-model="inputModel"
|
|
116
133
|
:placeholder="placeholder"
|
|
117
|
-
v-bind="
|
|
134
|
+
v-bind="inputAttrs"
|
|
118
135
|
class="bg-transparant h-10 w-full min-w-40 px-3 text-sm text-gray-900 placeholder-gray-400 focus:outline-none"
|
|
119
136
|
/>
|
|
120
137
|
</div>
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { computed, nextTick, useAttrs, useTemplateRef, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
const TEXTAREA_ID = 'textarea-' + Math.random().toString(36).slice(2, 10)
|
|
5
|
+
|
|
6
|
+
defineOptions({
|
|
7
|
+
name: 'NexxtTextarea',
|
|
8
|
+
// Same routing as NexxtInputField: `class` and `style` position the field in
|
|
9
|
+
// the consumer's layout and go to the wrapper, everything else describes the
|
|
10
|
+
// control and goes to the textarea. Nothing lands on both.
|
|
11
|
+
inheritAttrs: false,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export interface NexxtTextareaProps {
|
|
15
|
+
/** Placeholder shown while the field is empty. */
|
|
16
|
+
placeholder?: string
|
|
17
|
+
/** Label rendered above the field. */
|
|
18
|
+
label?: string
|
|
19
|
+
/** Put the field in its error state. */
|
|
20
|
+
error?: boolean
|
|
21
|
+
/** Message shown below the field while `error` is set. */
|
|
22
|
+
errorMessage?: string
|
|
23
|
+
/** Mark the label with an asterisk. */
|
|
24
|
+
required?: boolean
|
|
25
|
+
/**
|
|
26
|
+
* Visible rows, and the floor for `autosize`. The field is never shorter than
|
|
27
|
+
* a NexxtInputField, whatever this is set to.
|
|
28
|
+
*/
|
|
29
|
+
rows?: number
|
|
30
|
+
/**
|
|
31
|
+
* Grow with the content instead of scrolling, between `rows` and `maxRows`.
|
|
32
|
+
* Off by default: a field that changes height while typing moves whatever sits
|
|
33
|
+
* below it, so it should be asked for rather than assumed.
|
|
34
|
+
*/
|
|
35
|
+
autosize?: boolean
|
|
36
|
+
/** Upper bound for `autosize`, in rows. Unset means it grows indefinitely. */
|
|
37
|
+
maxRows?: number
|
|
38
|
+
/**
|
|
39
|
+
* Whether the field can be dragged taller by its corner grip. Named after the
|
|
40
|
+
* CSS property it sets, so it can grow to cover the other values later
|
|
41
|
+
* without changing what the two it has today mean.
|
|
42
|
+
*
|
|
43
|
+
* Ignored while `autosize` is on: the autosizer rewrites the height on every
|
|
44
|
+
* keystroke, so a dragged height would not survive the next character.
|
|
45
|
+
*/
|
|
46
|
+
resize?: 'vertical' | 'none'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const {
|
|
50
|
+
label,
|
|
51
|
+
error = false,
|
|
52
|
+
errorMessage,
|
|
53
|
+
required = false,
|
|
54
|
+
rows = 3,
|
|
55
|
+
autosize = false,
|
|
56
|
+
maxRows,
|
|
57
|
+
resize = 'vertical',
|
|
58
|
+
} = defineProps<NexxtTextareaProps>()
|
|
59
|
+
|
|
60
|
+
// `null` is in the type because the field's normal job is editing a nullable
|
|
61
|
+
// database column, and an API that returns one should not force every call site
|
|
62
|
+
// to write `?? ''`. The default stays: it only fires for `undefined`, and it
|
|
63
|
+
// keeps an unbound field from starting out null.
|
|
64
|
+
const model = defineModel<string | null>({ default: '' })
|
|
65
|
+
|
|
66
|
+
const attrs = useAttrs()
|
|
67
|
+
|
|
68
|
+
const wrapperAttrs = computed(() => ({ class: attrs.class, style: attrs.style }))
|
|
69
|
+
|
|
70
|
+
const controlAttrs = computed(() => {
|
|
71
|
+
const rest: Record<string, unknown> = { ...attrs }
|
|
72
|
+
delete rest.class
|
|
73
|
+
delete rest.style
|
|
74
|
+
return rest
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const textareaEl = useTemplateRef<HTMLTextAreaElement>('textareaEl')
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Height is measured, not calculated: reset to `auto` first so `scrollHeight`
|
|
81
|
+
* reports the content height rather than the height already set. `maxRows` is
|
|
82
|
+
* converted through the element's own computed line-height so it follows the
|
|
83
|
+
* field's typography instead of a hardcoded number.
|
|
84
|
+
*/
|
|
85
|
+
const fitToContent = () => {
|
|
86
|
+
const el = textareaEl.value
|
|
87
|
+
if (!el || !autosize) return
|
|
88
|
+
|
|
89
|
+
// Reset first, so scrollHeight reports the content height rather than the
|
|
90
|
+
// height already set.
|
|
91
|
+
el.style.height = 'auto'
|
|
92
|
+
|
|
93
|
+
const style = getComputedStyle(el)
|
|
94
|
+
const lineHeight = parseFloat(style.lineHeight) || 0
|
|
95
|
+
const padding = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom)
|
|
96
|
+
const border = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth)
|
|
97
|
+
|
|
98
|
+
// scrollHeight covers content plus padding, but `height` under border-box
|
|
99
|
+
// includes the border too. Without adding it the field renders a border's
|
|
100
|
+
// width too short -- small, but enough to sit lower than a NexxtInputField
|
|
101
|
+
// beside it.
|
|
102
|
+
const natural = el.scrollHeight + border
|
|
103
|
+
const cap = maxRows && lineHeight ? maxRows * lineHeight + padding + border : Infinity
|
|
104
|
+
|
|
105
|
+
el.style.height = `${Math.min(natural, cap)}px`
|
|
106
|
+
el.style.overflowY = natural > cap ? 'auto' : 'hidden'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Covers programmatic changes as well as typing, and waits for the DOM so
|
|
110
|
+
// scrollHeight is measured after the new value is rendered.
|
|
111
|
+
watch(model, () => nextTick(fitToContent), { immediate: true, flush: 'post' })
|
|
112
|
+
|
|
113
|
+
defineExpose({ textareaEl, fitToContent })
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<template>
|
|
117
|
+
<div class="inline-flex flex-col gap-1" :class="wrapperAttrs.class" :style="wrapperAttrs.style">
|
|
118
|
+
<label v-if="label" class="justify-start small-normal text-gray-700" :for="TEXTAREA_ID">
|
|
119
|
+
{{ label }}
|
|
120
|
+
<span v-if="required" class="text-brick-500">*</span>
|
|
121
|
+
</label>
|
|
122
|
+
|
|
123
|
+
<textarea
|
|
124
|
+
:id="TEXTAREA_ID"
|
|
125
|
+
ref="textareaEl"
|
|
126
|
+
v-model="model"
|
|
127
|
+
v-bind="controlAttrs"
|
|
128
|
+
:rows="rows"
|
|
129
|
+
:placeholder="placeholder"
|
|
130
|
+
class="min-h-10 w-full rounded-lg border px-3 py-2 small-normal text-gray-900 transition-colors outline-none placeholder:text-gray-400 focus:border-cornflower-blue-500"
|
|
131
|
+
:class="[
|
|
132
|
+
error ? 'border-brick-400' : 'border-gray-200',
|
|
133
|
+
// autosize wins: it owns the height, and a grip that fights it every
|
|
134
|
+
// keystroke is worse than no grip.
|
|
135
|
+
autosize || resize === 'none' ? 'resize-none' : 'resize-y',
|
|
136
|
+
]"
|
|
137
|
+
@input="fitToContent"
|
|
138
|
+
/>
|
|
139
|
+
|
|
140
|
+
<p v-if="error && errorMessage" class="small-normal text-brick-400">
|
|
141
|
+
{{ errorMessage }}
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
</template>
|
package/src/components.json
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"NexxtTable": "components/Table/Table.vue",
|
|
49
49
|
"NexxtTabs": "components/Tabs/Tabs.vue",
|
|
50
50
|
"NexxtTemplateCard": "components/TemplateCard/TemplateCard.vue",
|
|
51
|
+
"NexxtTextarea": "components/Textarea/Textarea.vue",
|
|
51
52
|
"NexxtTimelineEvent": "components/TimelineEvent/TimelineEvent.vue",
|
|
52
53
|
"NexxtTimelineLanes": "components/TimelineLanes/TimelineLanes.vue",
|
|
53
54
|
"NexxtTimelinePhaseblock": "components/TimelinePhaseblock/TimelinePhaseblock.vue",
|
package/src/index.ts
CHANGED
|
@@ -62,6 +62,7 @@ export { default as NexxtTabbedContent } from './components/TabbedContent/Tabbed
|
|
|
62
62
|
export { default as NexxtTable } from './components/Table/Table.vue'
|
|
63
63
|
export { default as NexxtTabs } from './components/Tabs/Tabs.vue'
|
|
64
64
|
export { default as NexxtTemplateCard } from './components/TemplateCard/TemplateCard.vue'
|
|
65
|
+
export { default as NexxtTextarea } from './components/Textarea/Textarea.vue'
|
|
65
66
|
export { default as NexxtTimelineEvent } from './components/TimelineEvent/TimelineEvent.vue'
|
|
66
67
|
export { default as NexxtTimelineLanes } from './components/TimelineLanes/TimelineLanes.vue'
|
|
67
68
|
export { default as NexxtTimelinePhaseblock } from './components/TimelinePhaseblock/TimelinePhaseblock.vue'
|