@meistrari/tela-build 1.47.3 → 1.48.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/components/tela/button/button.vue +6 -6
- package/components/tela/card/card.mdx +10 -8
- package/components/tela/card/card.stories.ts +19 -19
- package/components/tela/card/card.vue +7 -5
- package/components/tela/chart/chart-bar.vue +2 -2
- package/components/tela/create-card/create-card.mdx +84 -0
- package/components/tela/create-card/create-card.stories.ts +133 -0
- package/components/tela/create-card/create-card.vue +108 -0
- package/components/tela/filter/filter-trigger.vue +1 -1
- package/components/tela/home/home-body.vue +5 -0
- package/components/tela/home/home-content.vue +5 -0
- package/components/tela/home/home-section.vue +5 -0
- package/components/tela/home/home-title.vue +5 -0
- package/components/tela/home/home-toolbar.vue +10 -0
- package/components/tela/home/home.mdx +285 -0
- package/components/tela/home/home.vue +5 -0
- package/components/tela/home/metrics/metrics-card.vue +23 -0
- package/components/tela/home/metrics/metrics-group.vue +5 -0
- package/components/tela/home/metrics/metrics-stat.vue +23 -0
- package/components/tela/home/metrics/metrics.vue +18 -0
- package/components/tela/icon/custom.vue +9 -6
- package/components/tela/input/input.vue +1 -1
- package/components/tela/progress-bar/progress-bar.mdx +117 -0
- package/components/tela/progress-bar/progress-bar.stories.ts +148 -0
- package/components/tela/progress-bar/progress-bar.vue +88 -0
- package/components/tela/sidebar/sidebar-footer.vue +1 -1
- package/components/tela/sidebar/sidebar-item.vue +2 -1
- package/components/tela/sidebar/sidebar.mdx +2 -2
- package/components/tela/table/table-head.vue +1 -1
- package/components/tela/table/table-header.vue +1 -1
- package/components/tela/table/table-row.vue +1 -1
- package/package.json +1 -1
|
@@ -33,16 +33,16 @@ const sizeStyle = computed(() => (({
|
|
|
33
33
|
|
|
34
34
|
const variantStyle = computed(() => (({
|
|
35
35
|
'primary': fold`
|
|
36
|
-
bg-
|
|
37
|
-
hover:bg-
|
|
38
|
-
active:bg-
|
|
36
|
+
bg-neutral-900 text-white
|
|
37
|
+
hover:bg-neutral-800
|
|
38
|
+
active:bg-neutral-700
|
|
39
39
|
focus-visible:ring-0.5px focus-visible:ring-cyan-600
|
|
40
40
|
`,
|
|
41
41
|
'secondary': fold`
|
|
42
|
-
bg-white text-
|
|
42
|
+
bg-white text-neutral-900 border border-0.5px
|
|
43
43
|
[box-shadow:0_1px_6px_0_rgba(103,127,148,0.05)]
|
|
44
|
-
hover:bg-subtle hover:border-
|
|
45
|
-
active:bg-muted active:border-
|
|
44
|
+
hover:bg-subtle hover:border-strong
|
|
45
|
+
active:bg-muted active:border-neutral-400/60
|
|
46
46
|
focus-visible:ring-0.5px focus-visible:ring-cyan-600
|
|
47
47
|
`,
|
|
48
48
|
'ghost': fold`
|
|
@@ -14,16 +14,18 @@ A surface container component used to group related content with consistent visu
|
|
|
14
14
|
|
|
15
15
|
Always use the `size` prop to control card padding — it maps directly to the standardized values. Never apply padding manually to `<TelaCard>` or its inner elements.
|
|
16
16
|
|
|
17
|
+
Only `xs` and `sm` are available. Both share `rounded-12px`.
|
|
18
|
+
|
|
17
19
|
| `size` | Applied classes | Use for |
|
|
18
20
|
|--------|------------------|---------|
|
|
19
|
-
| `
|
|
20
|
-
| `sm` | `p-24px
|
|
21
|
+
| `xs` | `p-20px` | Compact cards, dense inner containers |
|
|
22
|
+
| `sm` *(default)* | `p-24px` | Standard cards |
|
|
21
23
|
|
|
22
24
|
```vue
|
|
23
|
-
<!-- Correct — use
|
|
24
|
-
<TelaCard size="
|
|
25
|
+
<!-- Correct — use xs or sm -->
|
|
26
|
+
<TelaCard size="xs">...</TelaCard>
|
|
25
27
|
<TelaCard size="sm">...</TelaCard>
|
|
26
|
-
<TelaCard>...</TelaCard> <!--
|
|
28
|
+
<TelaCard>...</TelaCard> <!-- sm is the default -->
|
|
27
29
|
|
|
28
30
|
<!-- Incorrect — never apply padding manually -->
|
|
29
31
|
<TelaCard class="p-20px">...</TelaCard>
|
|
@@ -43,10 +45,10 @@ Always use the `size` prop to control card padding — it maps directly to the s
|
|
|
43
45
|
</TelaCard>
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
###
|
|
48
|
+
### Compact / Inner Card
|
|
47
49
|
|
|
48
50
|
```vue
|
|
49
|
-
<TelaCard size="
|
|
51
|
+
<TelaCard size="xs">
|
|
50
52
|
<span class="body-12-medium text-secondary">Compact content</span>
|
|
51
53
|
</TelaCard>
|
|
52
54
|
```
|
|
@@ -85,7 +87,7 @@ Cards in grids must use `h-full` for consistent heights.
|
|
|
85
87
|
|
|
86
88
|
| Prop | Type | Default | Description |
|
|
87
89
|
|------|------|---------|-------------|
|
|
88
|
-
| `size` | `'
|
|
90
|
+
| `size` | `'xs' \| 'sm'` | `'sm'` | `xs` = `p-20px`, `sm` = `p-24px`. Both use `rounded-12px` |
|
|
89
91
|
|
|
90
92
|
## Slots
|
|
91
93
|
|
|
@@ -8,15 +8,15 @@ const meta: Meta<typeof Card> = {
|
|
|
8
8
|
layout: 'centered',
|
|
9
9
|
docs: {
|
|
10
10
|
description: {
|
|
11
|
-
component: 'A surface container that groups related content with consistent visual boundaries. Use the `size` prop to control padding — `
|
|
11
|
+
component: 'A surface container that groups related content with consistent visual boundaries. Use the `size` prop to control padding — `sm` for standard cards, `xs` for compact or inner containers.',
|
|
12
12
|
},
|
|
13
13
|
},
|
|
14
14
|
},
|
|
15
15
|
argTypes: {
|
|
16
16
|
size: {
|
|
17
17
|
control: 'select',
|
|
18
|
-
options: ['
|
|
19
|
-
description: '`
|
|
18
|
+
options: ['xs', 'sm'],
|
|
19
|
+
description: '`sm` applies standard card padding. `xs` applies compact/inner container padding.',
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
}
|
|
@@ -41,7 +41,7 @@ export const Default: Story = {
|
|
|
41
41
|
`,
|
|
42
42
|
}),
|
|
43
43
|
args: {
|
|
44
|
-
size: '
|
|
44
|
+
size: 'sm',
|
|
45
45
|
},
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -49,10 +49,10 @@ export const Standard: Story = {
|
|
|
49
49
|
render: () => ({
|
|
50
50
|
components: { Card },
|
|
51
51
|
template: `
|
|
52
|
-
<Card size="
|
|
52
|
+
<Card size="sm">
|
|
53
53
|
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 280px;">
|
|
54
54
|
<p class="heading-h4-semibold text-contrast">Standard Card</p>
|
|
55
|
-
<p class="body-14-regular text-secondary">Used for
|
|
55
|
+
<p class="body-14-regular text-secondary">Used for standard content surfaces.</p>
|
|
56
56
|
</div>
|
|
57
57
|
</Card>
|
|
58
58
|
`,
|
|
@@ -60,7 +60,7 @@ export const Standard: Story = {
|
|
|
60
60
|
parameters: {
|
|
61
61
|
docs: {
|
|
62
62
|
description: {
|
|
63
|
-
story: 'Default size (`
|
|
63
|
+
story: 'Default size (`sm`). Use for primary content surfaces.',
|
|
64
64
|
},
|
|
65
65
|
},
|
|
66
66
|
},
|
|
@@ -70,10 +70,10 @@ export const Minor: Story = {
|
|
|
70
70
|
render: () => ({
|
|
71
71
|
components: { Card },
|
|
72
72
|
template: `
|
|
73
|
-
<Card size="
|
|
73
|
+
<Card size="xs">
|
|
74
74
|
<div style="display: flex; flex-direction: column; gap: 8px; min-width: 240px;">
|
|
75
|
-
<p class="heading-h5-semibold text-contrast">
|
|
76
|
-
<p class="body-12-regular text-secondary">Used for
|
|
75
|
+
<p class="heading-h5-semibold text-contrast">Compact Card</p>
|
|
76
|
+
<p class="body-12-regular text-secondary">Used for compact cards and inner containers.</p>
|
|
77
77
|
</div>
|
|
78
78
|
</Card>
|
|
79
79
|
`,
|
|
@@ -81,7 +81,7 @@ export const Minor: Story = {
|
|
|
81
81
|
parameters: {
|
|
82
82
|
docs: {
|
|
83
83
|
description: {
|
|
84
|
-
story: '
|
|
84
|
+
story: 'Compact size (`xs`). Use for compact cards or nested inner containers.',
|
|
85
85
|
},
|
|
86
86
|
},
|
|
87
87
|
},
|
|
@@ -92,18 +92,18 @@ export const SizeComparison: Story = {
|
|
|
92
92
|
components: { Card },
|
|
93
93
|
template: `
|
|
94
94
|
<div style="display: flex; gap: 16px; align-items: flex-start;">
|
|
95
|
-
<Card size="
|
|
95
|
+
<Card size="sm">
|
|
96
96
|
<div style="display: flex; flex-direction: column; gap: 6px; min-width: 200px;">
|
|
97
|
-
<p class="body-12-medium text-secondary">size="
|
|
97
|
+
<p class="body-12-medium text-secondary">size="sm"</p>
|
|
98
98
|
<p class="heading-h4-semibold text-contrast">Standard</p>
|
|
99
|
-
<p class="body-14-regular text-tertiary">p-
|
|
99
|
+
<p class="body-14-regular text-tertiary">p-24px padding</p>
|
|
100
100
|
</div>
|
|
101
101
|
</Card>
|
|
102
|
-
<Card size="
|
|
102
|
+
<Card size="xs">
|
|
103
103
|
<div style="display: flex; flex-direction: column; gap: 6px; min-width: 200px;">
|
|
104
|
-
<p class="body-12-medium text-secondary">size="
|
|
105
|
-
<p class="heading-h4-semibold text-contrast">
|
|
106
|
-
<p class="body-14-regular text-tertiary">p-
|
|
104
|
+
<p class="body-12-medium text-secondary">size="xs"</p>
|
|
105
|
+
<p class="heading-h4-semibold text-contrast">Compact</p>
|
|
106
|
+
<p class="body-14-regular text-tertiary">p-20px padding</p>
|
|
107
107
|
</div>
|
|
108
108
|
</Card>
|
|
109
109
|
</div>
|
|
@@ -136,7 +136,7 @@ export const Grid: Story = {
|
|
|
136
136
|
layout: 'padded',
|
|
137
137
|
docs: {
|
|
138
138
|
description: {
|
|
139
|
-
story: 'Cards used in a grid layout. No padding override needed — `size="
|
|
139
|
+
story: 'Cards used in a grid layout. No padding override needed — `size="sm"` is the default.',
|
|
140
140
|
},
|
|
141
141
|
},
|
|
142
142
|
},
|
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { HTMLAttributes } from 'vue'
|
|
3
3
|
|
|
4
|
-
const props = withDefaults(defineProps<{
|
|
5
|
-
size?: '
|
|
4
|
+
const props = withDefaults(defineProps <{
|
|
5
|
+
size?: 'xs' | 'sm'
|
|
6
6
|
class?: HTMLAttributes['class']
|
|
7
7
|
/** @deprecated Use `class` instead */
|
|
8
8
|
contentPadding?: HTMLAttributes['class']
|
|
9
9
|
/** @deprecated Use `class` instead */
|
|
10
10
|
borderRadius?: HTMLAttributes['class']
|
|
11
11
|
}>(), {
|
|
12
|
-
size: '
|
|
12
|
+
size: 'sm',
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
const sizeStyles = computed(() => (({
|
|
16
|
+
xs: { padding: 'p-20px' },
|
|
16
17
|
sm: { padding: 'p-24px' },
|
|
18
|
+
/** @deprecated: Use 'xs' or 'sm' instead */
|
|
17
19
|
md: { padding: 'p-32px' },
|
|
18
|
-
}) as Record<string, { padding: string }>)[props.size ?? '
|
|
20
|
+
}) as Record<string, { padding: string }>)[props.size ?? 'sm'] ?? { padding: '' })
|
|
19
21
|
|
|
20
22
|
const paddingClass = computed(() => props.contentPadding ?? sizeStyles.value.padding)
|
|
21
23
|
const rootEl = ref<HTMLElement | null>(null)
|
|
@@ -26,7 +28,7 @@ defineExpose({
|
|
|
26
28
|
</script>
|
|
27
29
|
|
|
28
30
|
<template>
|
|
29
|
-
<div ref="rootEl" :class="cn('rounded-
|
|
31
|
+
<div ref="rootEl" :class="cn('rounded-12px bg border-0.5px border', paddingClass, props.class)">
|
|
30
32
|
<slot />
|
|
31
33
|
</div>
|
|
32
34
|
</template>
|
|
@@ -8,7 +8,7 @@ const props = withDefaults(defineProps<{
|
|
|
8
8
|
}>(), {
|
|
9
9
|
height: '12px',
|
|
10
10
|
gap: '2px',
|
|
11
|
-
duration:
|
|
11
|
+
duration: 400,
|
|
12
12
|
})
|
|
13
13
|
|
|
14
14
|
const animatedFilled = ref(0)
|
|
@@ -52,7 +52,7 @@ watch(() => props.filled, (newValue) => {
|
|
|
52
52
|
:style="{ height }"
|
|
53
53
|
w-1px
|
|
54
54
|
rounded-1px
|
|
55
|
-
:bg="bar.isFilled ? '
|
|
55
|
+
:bg="bar.isFilled ? 'green-500' : 'neutral-200'"
|
|
56
56
|
/>
|
|
57
57
|
</div>
|
|
58
58
|
</template>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# TelaCreateCard
|
|
2
|
+
|
|
3
|
+
A fixed-size entry point card for creating something new from files. Click it to open the native file picker, or drag files directly onto it. It emits a `change` event with the native input event so the parent owns the upload logic.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
- **Always** handle the `change` event — the component only surfaces files, it does not upload them.
|
|
8
|
+
- The card has a **fixed footprint** (`w-221px h-128px`). Lay it out in a grid or flex row alongside other cards; do not stretch it.
|
|
9
|
+
- Use it as the **first cell** in a list of existing items (the "add new" affordance), not as a standalone hero.
|
|
10
|
+
- Set `multiple={false}` when the flow only accepts a single file.
|
|
11
|
+
- Pass `name` when the input participates in a native `<form>` submission.
|
|
12
|
+
- Use `disabled` while an upload is in flight — it blocks both click and drag-and-drop.
|
|
13
|
+
|
|
14
|
+
## Examples
|
|
15
|
+
|
|
16
|
+
### Default
|
|
17
|
+
|
|
18
|
+
```vue
|
|
19
|
+
<TelaCreateCard @change="handleChange" />
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
function handleChange(event: Event) {
|
|
24
|
+
const input = event.target as HTMLInputElement
|
|
25
|
+
const files = input.files
|
|
26
|
+
// upload files...
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Custom Labels
|
|
31
|
+
|
|
32
|
+
```vue
|
|
33
|
+
<TelaCreateCard
|
|
34
|
+
title="Upload dataset"
|
|
35
|
+
description="Drop a CSV or"
|
|
36
|
+
browse-label="choose a file"
|
|
37
|
+
:multiple="false"
|
|
38
|
+
@change="handleChange"
|
|
39
|
+
/>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Without Browse Label
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<!-- Empty browse-label hides the underlined call-to-action -->
|
|
46
|
+
<TelaCreateCard
|
|
47
|
+
title="New canvas"
|
|
48
|
+
description="Drag files here"
|
|
49
|
+
browse-label=""
|
|
50
|
+
@change="handleChange"
|
|
51
|
+
/>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Disabled (e.g. while uploading)
|
|
55
|
+
|
|
56
|
+
```vue
|
|
57
|
+
<TelaCreateCard :disabled="isUploading" @change="handleChange" />
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### In a Grid
|
|
61
|
+
|
|
62
|
+
```vue
|
|
63
|
+
<div class="grid grid-cols-3 gap-4">
|
|
64
|
+
<TelaCreateCard @change="handleChange" />
|
|
65
|
+
<TelaCard v-for="item in items" :key="item.id" h-full>...</TelaCard>
|
|
66
|
+
</div>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Props
|
|
70
|
+
|
|
71
|
+
| Prop | Type | Default | Description |
|
|
72
|
+
|------|------|---------|-------------|
|
|
73
|
+
| `title` | `string` | `'New analysis'` | Heading shown inside the card |
|
|
74
|
+
| `description` | `string` | `'Drag files or'` | Leading helper text before the browse label |
|
|
75
|
+
| `browseLabel` | `string` | `'browse your computer'` | Underlined call-to-action appended after the description. Empty string hides it |
|
|
76
|
+
| `multiple` | `boolean` | `true` | Whether the file input accepts multiple files |
|
|
77
|
+
| `name` | `string` | — | `name` attribute forwarded to the underlying file input |
|
|
78
|
+
| `disabled` | `boolean` | `false` | Disables clicking and drag-and-drop, and dims the card |
|
|
79
|
+
|
|
80
|
+
## Events
|
|
81
|
+
|
|
82
|
+
| Event | Payload | Description |
|
|
83
|
+
|-------|---------|-------------|
|
|
84
|
+
| `change` | `Event` | Native input change event, fired on file selection and on drop. Read files from `(event.target as HTMLInputElement).files` |
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import CreateCard from './create-card.vue'
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof CreateCard> = {
|
|
5
|
+
title: 'Core/CreateCard',
|
|
6
|
+
component: CreateCard,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'centered',
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: 'A fixed-size entry point card for creating something new from files. Click to open the file picker, or drag files onto it. Emits `change` with the native input event.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
title: {
|
|
17
|
+
control: 'text',
|
|
18
|
+
description: 'Heading shown inside the card.',
|
|
19
|
+
},
|
|
20
|
+
description: {
|
|
21
|
+
control: 'text',
|
|
22
|
+
description: 'Leading helper text before the browse label.',
|
|
23
|
+
},
|
|
24
|
+
browseLabel: {
|
|
25
|
+
control: 'text',
|
|
26
|
+
description: 'Underlined call-to-action appended after the description.',
|
|
27
|
+
},
|
|
28
|
+
multiple: {
|
|
29
|
+
control: 'boolean',
|
|
30
|
+
description: 'Whether the file input accepts multiple files.',
|
|
31
|
+
},
|
|
32
|
+
name: {
|
|
33
|
+
control: 'text',
|
|
34
|
+
description: 'Name attribute forwarded to the underlying file input.',
|
|
35
|
+
},
|
|
36
|
+
disabled: {
|
|
37
|
+
control: 'boolean',
|
|
38
|
+
description: 'Disables clicking and drag-and-drop.',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
args: {
|
|
42
|
+
onChange: (event: Event) => {
|
|
43
|
+
const input = event.target as HTMLInputElement
|
|
44
|
+
// eslint-disable-next-line no-console
|
|
45
|
+
console.log('change', input.files)
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default meta
|
|
51
|
+
|
|
52
|
+
type Story = StoryObj<typeof meta>
|
|
53
|
+
|
|
54
|
+
export const Default: Story = {
|
|
55
|
+
render: args => ({
|
|
56
|
+
components: { CreateCard },
|
|
57
|
+
setup() {
|
|
58
|
+
return { args }
|
|
59
|
+
},
|
|
60
|
+
template: '<CreateCard v-bind="args" @change="args.onChange" />',
|
|
61
|
+
}),
|
|
62
|
+
args: {
|
|
63
|
+
title: 'New analysis',
|
|
64
|
+
description: 'Drag files or',
|
|
65
|
+
browseLabel: 'browse your computer',
|
|
66
|
+
multiple: true,
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const Disabled: Story = {
|
|
71
|
+
render: args => ({
|
|
72
|
+
components: { CreateCard },
|
|
73
|
+
setup() {
|
|
74
|
+
return { args }
|
|
75
|
+
},
|
|
76
|
+
template: '<CreateCard v-bind="args" @change="args.onChange" />',
|
|
77
|
+
}),
|
|
78
|
+
args: {
|
|
79
|
+
disabled: true,
|
|
80
|
+
},
|
|
81
|
+
parameters: {
|
|
82
|
+
docs: {
|
|
83
|
+
description: {
|
|
84
|
+
story: 'Disabled state — clicking and drag-and-drop are inert and the card is dimmed.',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const CustomLabels: Story = {
|
|
91
|
+
render: args => ({
|
|
92
|
+
components: { CreateCard },
|
|
93
|
+
setup() {
|
|
94
|
+
return { args }
|
|
95
|
+
},
|
|
96
|
+
template: '<CreateCard v-bind="args" @change="args.onChange" />',
|
|
97
|
+
}),
|
|
98
|
+
args: {
|
|
99
|
+
title: 'Upload dataset',
|
|
100
|
+
description: 'Drop a CSV or',
|
|
101
|
+
browseLabel: 'choose a file',
|
|
102
|
+
multiple: false,
|
|
103
|
+
},
|
|
104
|
+
parameters: {
|
|
105
|
+
docs: {
|
|
106
|
+
description: {
|
|
107
|
+
story: 'Custom title, description, and browse label. Set `multiple: false` to accept a single file.',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const NoBrowseLabel: Story = {
|
|
114
|
+
render: args => ({
|
|
115
|
+
components: { CreateCard },
|
|
116
|
+
setup() {
|
|
117
|
+
return { args }
|
|
118
|
+
},
|
|
119
|
+
template: '<CreateCard v-bind="args" @change="args.onChange" />',
|
|
120
|
+
}),
|
|
121
|
+
args: {
|
|
122
|
+
title: 'New canvas',
|
|
123
|
+
description: 'Drag files here',
|
|
124
|
+
browseLabel: '',
|
|
125
|
+
},
|
|
126
|
+
parameters: {
|
|
127
|
+
docs: {
|
|
128
|
+
description: {
|
|
129
|
+
story: 'Omitting `browseLabel` (empty string) hides the underlined call-to-action.',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = withDefaults(defineProps<{
|
|
3
|
+
disabled?: boolean
|
|
4
|
+
title?: string
|
|
5
|
+
description?: string
|
|
6
|
+
browseLabel?: string
|
|
7
|
+
multiple?: boolean
|
|
8
|
+
name?: string
|
|
9
|
+
}>(), {
|
|
10
|
+
title: 'New analysis',
|
|
11
|
+
description: 'Drag files or',
|
|
12
|
+
browseLabel: 'browse your computer',
|
|
13
|
+
multiple: true,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const emit = defineEmits<{
|
|
17
|
+
(e: 'change', event: Event): void
|
|
18
|
+
}>()
|
|
19
|
+
|
|
20
|
+
const fileInputRef = ref<HTMLInputElement | null>(null)
|
|
21
|
+
|
|
22
|
+
const dragDepth = ref(0)
|
|
23
|
+
const isDragging = computed(() => dragDepth.value > 0)
|
|
24
|
+
|
|
25
|
+
function openFilePicker() {
|
|
26
|
+
if (props.disabled)
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
fileInputRef.value?.click()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleFileChange(event: Event) {
|
|
33
|
+
emit('change', event)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function onDragEnter() {
|
|
37
|
+
if (props.disabled)
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
dragDepth.value++
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function onDragLeave() {
|
|
44
|
+
if (props.disabled)
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
dragDepth.value = Math.max(0, dragDepth.value - 1)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function onDrop(event: DragEvent) {
|
|
51
|
+
dragDepth.value = 0
|
|
52
|
+
|
|
53
|
+
if (props.disabled)
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
const files = event.dataTransfer?.files
|
|
57
|
+
|
|
58
|
+
if (files?.length && fileInputRef.value) {
|
|
59
|
+
const dataTransfer = new DataTransfer()
|
|
60
|
+
const droppedFiles = props.multiple ? Array.from(files) : Array.from(files).slice(0, 1)
|
|
61
|
+
|
|
62
|
+
for (const file of droppedFiles)
|
|
63
|
+
dataTransfer.items.add(file)
|
|
64
|
+
|
|
65
|
+
fileInputRef.value.files = dataTransfer.files
|
|
66
|
+
fileInputRef.value.dispatchEvent(new Event('change', { bubbles: true }))
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template>
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
:disabled="disabled"
|
|
75
|
+
relative w-221px h-128px rounded-12px p-20px
|
|
76
|
+
bg border-0.5px border
|
|
77
|
+
flex="~ col" items-start justify-end text-start
|
|
78
|
+
hover:bg-subtle hover:border-strong active:bg-muted disabled:opacity-50 disabled:cursor-not-allowed
|
|
79
|
+
:class="isDragging ? 'border-dashed border-neutral-400 bg-subtle' : 'active:border-neutral-400/60'"
|
|
80
|
+
@click="openFilePicker"
|
|
81
|
+
@dragenter.prevent="onDragEnter"
|
|
82
|
+
@dragover.prevent
|
|
83
|
+
@dragleave.prevent="onDragLeave"
|
|
84
|
+
@drop.prevent="onDrop"
|
|
85
|
+
>
|
|
86
|
+
<TelaIcon name="i-ph-plus" size="20px" color="icon" absolute top-16px right-16px />
|
|
87
|
+
<div flex="~ col" items-start justify-end gap-4px>
|
|
88
|
+
<h5 heading-h5-semibold text-primary>
|
|
89
|
+
{{ title }}
|
|
90
|
+
</h5>
|
|
91
|
+
<div leading-none>
|
|
92
|
+
<span body-12-regular text-secondary>
|
|
93
|
+
{{ description }}
|
|
94
|
+
</span>
|
|
95
|
+
<span v-if="browseLabel" block body-12-regular leading-none text-secondary underline underline-offset-3>{{ browseLabel }}</span>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</button>
|
|
99
|
+
|
|
100
|
+
<input
|
|
101
|
+
ref="fileInputRef"
|
|
102
|
+
type="file"
|
|
103
|
+
:multiple="multiple"
|
|
104
|
+
:name="name"
|
|
105
|
+
hidden
|
|
106
|
+
@change="handleFileChange"
|
|
107
|
+
>
|
|
108
|
+
</template>
|
|
@@ -48,7 +48,7 @@ defineEmits<{
|
|
|
48
48
|
name="i-ph-caret-down-bold"
|
|
49
49
|
size="12px"
|
|
50
50
|
color="icon-secondary"
|
|
51
|
-
class="shrink-0 transition-all ease-out duration-
|
|
51
|
+
class="shrink-0 transition-all ease-out duration-150 group-data-[active]:rotate-180"
|
|
52
52
|
/>
|
|
53
53
|
</button>
|
|
54
54
|
</template>
|