@meistrari/tela-build 1.47.2 → 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.
Files changed (44) hide show
  1. package/components/tela/button/button.vue +6 -6
  2. package/components/tela/card/card.mdx +10 -8
  3. package/components/tela/card/card.stories.ts +19 -19
  4. package/components/tela/card/card.vue +7 -5
  5. package/components/tela/chart/chart-bar.vue +2 -2
  6. package/components/tela/combobox/combobox-input.vue +1 -1
  7. package/components/tela/combobox/combobox-label.vue +1 -1
  8. package/components/tela/combobox/combobox.vue +29 -29
  9. package/components/tela/create-card/create-card.mdx +84 -0
  10. package/components/tela/create-card/create-card.stories.ts +133 -0
  11. package/components/tela/create-card/create-card.vue +108 -0
  12. package/components/tela/expandable-input.vue +4 -5
  13. package/components/tela/filter/filter-trigger.vue +1 -1
  14. package/components/tela/home/home-body.vue +5 -0
  15. package/components/tela/home/home-content.vue +5 -0
  16. package/components/tela/home/home-section.vue +5 -0
  17. package/components/tela/home/home-title.vue +5 -0
  18. package/components/tela/home/home-toolbar.vue +10 -0
  19. package/components/tela/home/home.mdx +285 -0
  20. package/components/tela/home/home.vue +5 -0
  21. package/components/tela/home/metrics/metrics-card.vue +23 -0
  22. package/components/tela/home/metrics/metrics-group.vue +5 -0
  23. package/components/tela/home/metrics/metrics-stat.vue +23 -0
  24. package/components/tela/home/metrics/metrics.vue +18 -0
  25. package/components/tela/icon/custom.vue +9 -6
  26. package/components/tela/icon-button/icon-button.vue +3 -3
  27. package/components/tela/input/input.vue +1 -1
  28. package/components/tela/progress-bar/progress-bar.mdx +117 -0
  29. package/components/tela/progress-bar/progress-bar.stories.ts +148 -0
  30. package/components/tela/progress-bar/progress-bar.vue +88 -0
  31. package/components/tela/segment-toggle/segment-toggle.vue +95 -21
  32. package/components/tela/select-menu/select-menu-item.vue +2 -2
  33. package/components/tela/select-menu/select-menu-trigger.vue +3 -3
  34. package/components/tela/select-menu/select-menu.vue +4 -4
  35. package/components/tela/sidebar/sidebar-footer.vue +1 -1
  36. package/components/tela/sidebar/sidebar-item.vue +2 -1
  37. package/components/tela/sidebar/sidebar.mdx +2 -2
  38. package/components/tela/table/table-head.vue +1 -1
  39. package/components/tela/table/table-header.vue +1 -1
  40. package/components/tela/table/table-row.vue +1 -1
  41. package/components/tela/tabs/tabs-indicator.vue +1 -1
  42. package/components/tela/tabs/tabs-list.vue +1 -1
  43. package/components/tela/tabs/tabs-trigger.vue +1 -1
  44. 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-gray-900 text-white
37
- hover:bg-gray-800
38
- active:bg-gray-700
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-gray-900 border border-0.5px
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-gray-300
45
- active:bg-muted active:border-gray-400/60
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
- | `md` *(default)* | `p-32px sm:p-48px rounded-24px` | Standard and large cards |
20
- | `sm` | `p-24px rounded-12px` | Small cards, inner containers |
21
+ | `xs` | `p-20px` | Compact cards, dense inner containers |
22
+ | `sm` *(default)* | `p-24px` | Standard cards |
21
23
 
22
24
  ```vue
23
- <!-- Correct — use size prop -->
24
- <TelaCard size="md">...</TelaCard>
25
+ <!-- Correct — use xs or sm -->
26
+ <TelaCard size="xs">...</TelaCard>
25
27
  <TelaCard size="sm">...</TelaCard>
26
- <TelaCard>...</TelaCard> <!-- md is the default -->
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
- ### Minor / Inner Card
48
+ ### Compact / Inner Card
47
49
 
48
50
  ```vue
49
- <TelaCard size="sm">
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` | `'sm' \| 'md'` | `'md'` | `md` = `p-32px sm:p-48px rounded-24px`, `sm` = `p-24px rounded-12px` |
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 — `md` for standard cards, `sm` for minor or inner containers.',
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: ['md', 'sm'],
19
- description: '`md` applies standard card padding. `sm` applies minor/inner container padding.',
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: 'md',
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="md">
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 large, standard, and medium cards.</p>
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 (`md`). Use for primary content surfaces.',
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="sm">
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">Minor Card</p>
76
- <p class="body-12-regular text-secondary">Used for small cards and inner containers.</p>
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: 'Small size (`sm`). Use for compact cards or nested inner containers.',
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="md">
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="md"</p>
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-32px sm:p-48px padding</p>
99
+ <p class="body-14-regular text-tertiary">p-24px padding</p>
100
100
  </div>
101
101
  </Card>
102
- <Card size="sm">
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="sm"</p>
105
- <p class="heading-h4-semibold text-contrast">Minor</p>
106
- <p class="body-14-regular text-tertiary">p-24px padding</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="md"` is the default.',
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?: 'sm' | 'md'
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: 'md',
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 ?? 'md'] ?? { padding: '' })
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-16px bg border-0.5px border', paddingClass, props.class)">
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: 1000,
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 ? '#5DC16B' : '#DFE3E7'"
55
+ :bg="bar.isFilled ? 'green-500' : 'neutral-200'"
56
56
  />
57
57
  </div>
58
58
  </template>
@@ -22,7 +22,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
22
22
  <template>
23
23
  <ComboboxInput
24
24
  v-bind="forwarded"
25
- :class="cn('flex h-10 w-full bg-white-1000 border-b-[0.5px] border-gray-200 rounded-xl px-3 py-1 body-14-regular shadow-sm text-gray-900 transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-400 focus-visible:bg-white-1000 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50', props.class)"
25
+ :class="cn('flex h-10 w-full bg border-b-[0.5px] border-border rounded-xl px-3 py-1 body-14-regular text-primary transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-tertiary focus-visible:bg focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50', props.class)"
26
26
  >
27
27
  <slot />
28
28
  </ComboboxInput>
@@ -15,7 +15,7 @@ const forwarded = useForwardProps(delegatedProps)
15
15
  <template>
16
16
  <ComboboxLabel
17
17
  v-bind="forwarded"
18
- :class="cn('px-2 py-1 body-12-semibold text-gray-500', props.class)"
18
+ :class="cn('px-2 py-1 body-12-semibold text-secondary', props.class)"
19
19
  >
20
20
  <slot />
21
21
  </ComboboxLabel>
@@ -366,7 +366,7 @@ watch(innerValue, (raw) => {
366
366
  <ComboboxList :class="cn('z-999', compact ? 'w-440px' : 'w-327px', props.contentClass)" :disable-portal="props.disablePortal" align="start">
367
367
  <div v-if="hasSearchbar" class="relative">
368
368
  <span class="absolute inset-y-0 start-0 flex items-center justify-center px-2.5">
369
- <TelaIcon name="i-ph-magnifying-glass" :size="compact ? '12px' : '14px'" class="text-gray-400" />
369
+ <TelaIcon name="i-ph-magnifying-glass" :size="compact ? '12px' : '14px'" class="text-tertiary" />
370
370
  </span>
371
371
  <ComboboxInput
372
372
  v-model="search" :class="cn('rounded-b-none', compact ? 'pl-7' : 'pl-8')"
@@ -374,7 +374,7 @@ watch(innerValue, (raw) => {
374
374
  />
375
375
  <div v-if="search.length > 0" class="absolute inset-y-0 end-0 flex items-center justify-center px-2.5">
376
376
  <button class="flex items-center justify-center" @click="handleDeleteSearchValue">
377
- <TelaIcon name="i-ph-x-circle" size="16px" class="text-gray-400" />
377
+ <TelaIcon name="i-ph-x-circle" size="16px" class="text-tertiary" />
378
378
  </button>
379
379
  </div>
380
380
  </div>
@@ -392,17 +392,17 @@ watch(innerValue, (raw) => {
392
392
  class="flex flex-col items-center justify-center gap-5 px-7"
393
393
  >
394
394
  <div class="flex flex-col items-center justify-center gap-3">
395
- <TelaIcon name="i-ph-smiley-sad" size="24px" class="text-gray-400" />
395
+ <TelaIcon name="i-ph-smiley-sad" size="24px" class="text-tertiary" />
396
396
  <span class="text-xl font-medium leading-none">
397
397
  {{ search.trim() && labelNoResultsWithSearch ? labelNoResultsWithSearch(search) : labelNoResults }}
398
398
  </span>
399
- <span v-if="search.trim()" class="inline-flex items-center gap-1.5 text-md text-gray-500">
399
+ <span v-if="search.trim()" class="inline-flex items-center gap-1.5 text-md text-secondary">
400
400
  {{ labelPress }}
401
401
  <div class="flex items-center gap-1">
402
- <div class="p-2px bg-gray-200 rounded">
402
+ <div class="p-2px bg-lowered rounded">
403
403
  <TelaIcon name="i-ph-command" size="16px" />
404
404
  </div>
405
- <div class="p-2px bg-gray-200 rounded">
405
+ <div class="p-2px bg-lowered rounded">
406
406
  <TelaIcon name="i-ph-backspace" size="16px" />
407
407
  </div>
408
408
  </div>
@@ -441,15 +441,15 @@ watch(innerValue, (raw) => {
441
441
  <div class="flex flex-col">
442
442
  <div class="flex items-center gap-1">
443
443
  <span :class="cn('font-medium truncate max-w-140px', compact ? 'text-sm font-580' : 'text-body-14-regular', labelItemClass)">{{ item.label }}</span>
444
- <TelaBadge v-if="item.isMultiModal" variant="filled" class="py-[2px] bg-gray-100" text-class="leading-none">
444
+ <TelaBadge v-if="item.isMultiModal" variant="filled" class="py-[2px] bg-muted" text-class="leading-none">
445
445
  {{ labelMultimodal }}
446
446
  </TelaBadge>
447
447
  <span v-if="item.cost !== undefined" class="text-10px font-580 ml-1px">
448
- <span class="text-black-900">{{ renderCostIndicator(item.cost)?.blackSymbols }}</span>
449
- <span class="text-gray-300">{{ renderCostIndicator(item.cost)?.graySymbols }}</span>
448
+ <span class="text-primary">{{ renderCostIndicator(item.cost)?.blackSymbols }}</span>
449
+ <span class="text-neutral-300">{{ renderCostIndicator(item.cost)?.graySymbols }}</span>
450
450
  </span>
451
451
  </div>
452
- <span v-if="item.description" :class="cn('font-normal text-sm text-balance leading-none text-gray-500', descriptionClass)">
452
+ <span v-if="item.description" :class="cn('font-normal text-sm text-balance leading-none text-secondary', descriptionClass)">
453
453
  {{ item.description }}
454
454
  </span>
455
455
  <slot name="tags" :option="item" />
@@ -457,18 +457,18 @@ watch(innerValue, (raw) => {
457
457
  </div>
458
458
  <div class="flex gap-3">
459
459
  <div v-if="item.maxInputTokens" class="flex flex-col gap-1.5">
460
- <span class="text-gray-400 leading-none text-9px uppercase tracking-wider font-semibold">
460
+ <span class="text-tertiary leading-none text-9px uppercase tracking-wider font-semibold">
461
461
  {{ labelInputMax }}
462
462
  </span>
463
- <span class="text-gray-400 leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-gray-700">
463
+ <span class="text-tertiary leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-neutral-700">
464
464
  {{ handleFormatNumber(item.maxInputTokens) }}
465
465
  </span>
466
466
  </div>
467
467
  <div v-if="item.maxOutputTokens" class="flex flex-col gap-1.5">
468
- <span class="text-gray-400 leading-none text-9px uppercase tracking-wider font-semibold">
468
+ <span class="text-tertiary leading-none text-9px uppercase tracking-wider font-semibold">
469
469
  {{ labelOutputMax }}
470
470
  </span>
471
- <span class="text-gray-400 leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-gray-700">
471
+ <span class="text-tertiary leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-neutral-700">
472
472
  {{ handleFormatNumber(item.maxOutputTokens) }}
473
473
  </span>
474
474
  </div>
@@ -492,17 +492,17 @@ watch(innerValue, (raw) => {
492
492
  class="flex flex-col items-center justify-center gap-5 px-7"
493
493
  >
494
494
  <div class="flex flex-col items-center justify-center gap-3">
495
- <TelaIcon name="i-ph-smiley-sad" size="24px" class="text-gray-400" />
495
+ <TelaIcon name="i-ph-smiley-sad" size="24px" class="text-tertiary" />
496
496
  <span class="text-xl font-medium leading-none">
497
497
  {{ search.trim() && labelNoResultsWithSearch ? labelNoResultsWithSearch(search) : labelNoResults }}
498
498
  </span>
499
- <span v-if="search.trim()" class="inline-flex gap-1.5 text-md text-gray-500">
499
+ <span v-if="search.trim()" class="inline-flex gap-1.5 text-md text-secondary">
500
500
  {{ labelPress }}
501
501
  <div class="flex items-center gap-1">
502
- <div class="p-2px bg-gray-200 rounded">
502
+ <div class="p-2px bg-lowered rounded">
503
503
  <TelaIcon name="i-ph-command" size="16px" />
504
504
  </div>
505
- <div class="p-2px bg-gray-200 rounded">
505
+ <div class="p-2px bg-lowered rounded">
506
506
  <TelaIcon name="i-ph-backspace" size="16px" />
507
507
  </div>
508
508
  </div>
@@ -548,15 +548,15 @@ watch(innerValue, (raw) => {
548
548
  <div class="flex flex-col">
549
549
  <div class="flex items-center gap-1">
550
550
  <span :class="cn('font-medium truncate max-w-140px', compact ? 'text-sm font-580' : 'text-body-14-regular', labelItemClass)">{{ item.label }}</span>
551
- <TelaBadge v-if="item.isMultiModal" variant="filled" class="py-[2px] bg-gray-100" text-class="leading-none">
551
+ <TelaBadge v-if="item.isMultiModal" variant="filled" class="py-[2px] bg-muted" text-class="leading-none">
552
552
  {{ labelMultimodal }}
553
553
  </TelaBadge>
554
554
  <span v-if="item.cost !== undefined" class="text-10px font-580 ml-1px">
555
- <span class="text-black-900">{{ renderCostIndicator(item.cost)?.blackSymbols }}</span>
556
- <span class="text-gray-300">{{ renderCostIndicator(item.cost)?.graySymbols }}</span>
555
+ <span class="text-primary">{{ renderCostIndicator(item.cost)?.blackSymbols }}</span>
556
+ <span class="text-neutral-300">{{ renderCostIndicator(item.cost)?.graySymbols }}</span>
557
557
  </span>
558
558
  </div>
559
- <span v-if="item.description" :class="cn('font-normal text-sm leading-none text-gray-500', descriptionClass)">
559
+ <span v-if="item.description" :class="cn('font-normal text-sm leading-none text-secondary', descriptionClass)">
560
560
  {{ item.description }}
561
561
  </span>
562
562
  <slot name="tags" :option="item" />
@@ -564,25 +564,25 @@ watch(innerValue, (raw) => {
564
564
  </div>
565
565
  <div v-if="item.maxInputTokens || item.maxOutputTokens" class="flex gap-3">
566
566
  <div class="flex flex-col gap-1.5">
567
- <span class="text-gray-400 leading-none text-9px uppercase tracking-wider font-semibold">
567
+ <span class="text-tertiary leading-none text-9px uppercase tracking-wider font-semibold">
568
568
  {{ labelInputMax }}
569
569
  </span>
570
- <span class="text-gray-400 leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-gray-700">
570
+ <span class="text-tertiary leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-neutral-700">
571
571
  {{ handleFormatNumber(item.maxInputTokens) }}
572
572
  </span>
573
573
  </div>
574
574
  <div class="flex flex-col gap-1.5">
575
- <span class="text-gray-400 leading-none text-9px uppercase tracking-wider font-semibold">
575
+ <span class="text-tertiary leading-none text-9px uppercase tracking-wider font-semibold">
576
576
  {{ labelOutputMax }}
577
577
  </span>
578
- <span class="text-gray-400 leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-gray-700">
578
+ <span class="text-tertiary leading-none text-11px uppercase font-semibold group-data-[highlighted]:text-neutral-700">
579
579
  {{ handleFormatNumber(item.maxOutputTokens) }}
580
580
  </span>
581
581
  </div>
582
582
  </div>
583
583
  </div>
584
584
  <div class="flex gap-2 items-center">
585
- <TelaIcon v-if="item.children?.length" name="i-ph-caret-right" size="14px" class="text-gray-400" />
585
+ <TelaIcon v-if="item.children?.length" name="i-ph-caret-right" size="14px" color="icon-tertiary" />
586
586
  <ComboboxItemIndicator v-else>
587
587
  <TelaIcon name="i-ph-check" size="14px" />
588
588
  </ComboboxItemIndicator>
@@ -602,7 +602,7 @@ watch(innerValue, (raw) => {
602
602
  v-for="child in item.children"
603
603
  :key="child.value"
604
604
  :class="cn(
605
- 'relative flex cursor-pointer select-none justify-between items-center rounded-lg px-2 py-2 text-sm font-medium text-gray-900 outline-none hover:bg-gray-100',
605
+ 'relative flex cursor-pointer select-none justify-between items-center rounded-lg px-2 py-2 text-sm font-medium text-primary outline-none hover:bg-muted',
606
606
  !compact && '!px-1.5',
607
607
  )"
608
608
  @click.stop="handleChildSelect(child, $event)"
@@ -617,7 +617,7 @@ watch(innerValue, (raw) => {
617
617
  </div>
618
618
  <div class="flex flex-col">
619
619
  <span :class="cn('font-medium truncate max-w-140px', compact ? 'text-sm font-580' : 'text-body-14-regular', labelItemClass)">{{ child.label }}</span>
620
- <span v-if="child.description" :class="cn('font-normal text-sm leading-none text-gray-500', descriptionClass)">
620
+ <span v-if="child.description" :class="cn('font-normal text-sm leading-none text-secondary', descriptionClass)">
621
621
  {{ child.description }}
622
622
  </span>
623
623
  </div>
@@ -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
+ }