ads-nuxt 0.1.1 → 0.1.2
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/LICENSE +21 -21
- package/README.md +567 -546
- package/app/app.config.ts +25 -25
- package/app/assets/ads/checkbox-check.svg +5 -5
- package/app/assets/ads/checkbox-minus.svg +5 -5
- package/app/assets/brand/farmarcas.svg +6 -6
- package/app/assets/brand/radar.svg +9 -9
- package/app/assets/css/component-tokens.css +403 -403
- package/app/assets/css/main.css +617 -617
- package/app/components/AdsBadge.vue +48 -48
- package/app/components/AdsBroadcast.vue +45 -45
- package/app/components/AdsButton.vue +569 -569
- package/app/components/AdsButtonGroup.vue +39 -39
- package/app/components/AdsButtonMenu.vue +48 -48
- package/app/components/AdsCard.vue +117 -117
- package/app/components/AdsCharttip.vue +78 -78
- package/app/components/AdsCheckbox.vue +77 -77
- package/app/components/AdsDatePicker.vue +473 -473
- package/app/components/AdsDropdownMenu.vue +19 -19
- package/app/components/AdsEmptyState.vue +58 -58
- package/app/components/AdsFarmarcasPill.vue +74 -74
- package/app/components/AdsFeatureCard.vue +217 -217
- package/app/components/AdsFileCard.vue +101 -101
- package/app/components/AdsFilterDropdown.vue +174 -174
- package/app/components/AdsFilterOption.vue +72 -72
- package/app/components/AdsFilterSelect.vue +202 -202
- package/app/components/AdsFooter.vue +25 -25
- package/app/components/AdsInput.vue +143 -143
- package/app/components/AdsListDropdown.vue +44 -44
- package/app/components/AdsLoading.vue +109 -109
- package/app/components/AdsMenuBar.vue +103 -103
- package/app/components/AdsMetricCard.vue +119 -119
- package/app/components/AdsMiniPill.vue +40 -40
- package/app/components/AdsModal.vue +157 -157
- package/app/components/AdsMonthPicker.vue +80 -80
- package/app/components/AdsNavbarApp.vue +67 -67
- package/app/components/AdsNavbarDesk.vue +62 -62
- package/app/components/AdsNotabilityPill.vue +27 -27
- package/app/components/AdsNumberInput.vue +166 -166
- package/app/components/AdsPagination.vue +91 -91
- package/app/components/AdsProductTag.vue +61 -61
- package/app/components/AdsRadio.vue +70 -70
- package/app/components/AdsSearchInput.vue +113 -113
- package/app/components/AdsSegmentedTabs.vue +28 -28
- package/app/components/AdsSideMenu.vue +101 -101
- package/app/components/AdsSmalltip.vue +44 -44
- package/app/components/AdsSteptip.vue +101 -101
- package/app/components/AdsSwitch.vue +59 -59
- package/app/components/AdsTable.vue +100 -100
- package/app/components/AdsTableToolbar.vue +44 -44
- package/app/components/AdsTabs.vue +51 -51
- package/app/components/AdsTextarea.vue +109 -109
- package/app/components/AdsToast.vue +181 -181
- package/app/components/AdsTooltip.vue +66 -66
- package/app/components/InvertedTooltip.vue +40 -40
- package/app/components/OverlayLoading.vue +173 -173
- package/app/components/ProgressBarLoading.vue +256 -256
- package/app/composables/useOverlayLoading.ts +66 -66
- package/app/locale/pt-br.ts +6 -6
- package/app/plugins/ads-toast.client.ts +24 -24
- package/app/theme/ads-theme.ts +370 -370
- package/app/theme/component-tokens.json +402 -402
- package/app/theme/component-tokens.ts +815 -815
- package/app/types/overlay-loading.types.ts +26 -26
- package/app/utils/ads-filter-select.ts +172 -172
- package/app/utils/ads-table.ts +204 -204
- package/app/utils/calendar-date.ts +128 -128
- package/app/utils/category-role.ts +45 -45
- package/app/utils/money.ts +77 -77
- package/app/utils/notability.ts +8 -8
- package/app/utils/parser.ts +134 -134
- package/app/utils/percentage.ts +54 -54
- package/nuxt.config.ts +31 -32
- package/package.json +48 -46
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<header class="flex h-[var(--ads-navbar-desk-height)] w-full items-center justify-between border-b border-[var(--ads-navbar-desk-border)] bg-[var(--ads-surface-card)] px-[var(--ads-navbar-desk-padding-x)] shadow-[0_2px_4px_-1px_color-mix(in_srgb,var(--color-ads-neutro-medio-60)_75%,transparent)]">
|
|
3
|
-
<div class="flex items-center gap-3">
|
|
4
|
-
<img :src="radarUrl" alt="RADAR" class="h-[21px] w-auto" />
|
|
5
|
-
<AdsProductTag v-if="product" :product="product" :label="productLabel" />
|
|
6
|
-
</div>
|
|
7
|
-
<div class="flex items-center gap-2">
|
|
8
|
-
<AdsBroadcast v-if="showBroadcast" />
|
|
9
|
-
<button
|
|
10
|
-
v-for="action in actions"
|
|
11
|
-
:key="action.id"
|
|
12
|
-
type="button"
|
|
13
|
-
class="flex size-[var(--ads-navbar-desk-action)] items-center justify-center rounded-[var(--ads-radius-xsm)] p-2 text-[var(--ads-text-secondary)] hover:bg-ads-neutro-claro-20 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ads-text-brand)]"
|
|
14
|
-
:aria-label="action.label"
|
|
15
|
-
@click="emit('action', action.id)"
|
|
16
|
-
>
|
|
17
|
-
<UIcon :name="action.icon" class="size-[18px]" />
|
|
18
|
-
</button>
|
|
19
|
-
<span class="flex size-[var(--ads-navbar-desk-avatar)] items-center justify-center overflow-hidden rounded-full bg-ads-neutro-claro-30" aria-hidden="true">
|
|
20
|
-
<img v-if="avatarUrl" :src="avatarUrl" alt="" class="size-full object-cover" />
|
|
21
|
-
<UIcon v-else name="i-lucide-user" class="size-4 text-[var(--ads-text-secondary)]" />
|
|
22
|
-
</span>
|
|
23
|
-
</div>
|
|
24
|
-
</header>
|
|
25
|
-
</template>
|
|
26
|
-
|
|
27
|
-
<script setup lang="ts">
|
|
28
|
-
import radarUrl from "../assets/brand/radar.svg?url";
|
|
29
|
-
import AdsBroadcast from "./AdsBroadcast.vue";
|
|
30
|
-
import AdsProductTag, { type AdsProductTagName } from "./AdsProductTag.vue";
|
|
31
|
-
|
|
32
|
-
export type AdsNavbarDeskAction = {
|
|
33
|
-
id: string;
|
|
34
|
-
label: string;
|
|
35
|
-
icon: string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
withDefaults(
|
|
39
|
-
defineProps<{
|
|
40
|
-
product?: AdsProductTagName | null;
|
|
41
|
-
productLabel?: string;
|
|
42
|
-
showBroadcast?: boolean;
|
|
43
|
-
avatarUrl?: string;
|
|
44
|
-
actions?: AdsNavbarDeskAction[];
|
|
45
|
-
}>(),
|
|
46
|
-
{
|
|
47
|
-
product: "performance",
|
|
48
|
-
productLabel: "performance",
|
|
49
|
-
showBroadcast: true,
|
|
50
|
-
avatarUrl: "",
|
|
51
|
-
actions: () => [
|
|
52
|
-
{ id: "alerts", label: "Alertas", icon: "i-lucide-bell" },
|
|
53
|
-
{ id: "settings", label: "Configurações", icon: "i-lucide-settings" },
|
|
54
|
-
{ id: "apps", label: "Aplicativos", icon: "i-lucide-layout-grid" },
|
|
55
|
-
],
|
|
56
|
-
},
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
const emit = defineEmits<{
|
|
60
|
-
action: [id: string];
|
|
61
|
-
}>();
|
|
62
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<header class="flex h-[var(--ads-navbar-desk-height)] w-full items-center justify-between border-b border-[var(--ads-navbar-desk-border)] bg-[var(--ads-surface-card)] px-[var(--ads-navbar-desk-padding-x)] shadow-[0_2px_4px_-1px_color-mix(in_srgb,var(--color-ads-neutro-medio-60)_75%,transparent)]">
|
|
3
|
+
<div class="flex items-center gap-3">
|
|
4
|
+
<img :src="radarUrl" alt="RADAR" class="h-[21px] w-auto" />
|
|
5
|
+
<AdsProductTag v-if="product" :product="product" :label="productLabel" />
|
|
6
|
+
</div>
|
|
7
|
+
<div class="flex items-center gap-2">
|
|
8
|
+
<AdsBroadcast v-if="showBroadcast" />
|
|
9
|
+
<button
|
|
10
|
+
v-for="action in actions"
|
|
11
|
+
:key="action.id"
|
|
12
|
+
type="button"
|
|
13
|
+
class="flex size-[var(--ads-navbar-desk-action)] items-center justify-center rounded-[var(--ads-radius-xsm)] p-2 text-[var(--ads-text-secondary)] hover:bg-ads-neutro-claro-20 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--ads-text-brand)]"
|
|
14
|
+
:aria-label="action.label"
|
|
15
|
+
@click="emit('action', action.id)"
|
|
16
|
+
>
|
|
17
|
+
<UIcon :name="action.icon" class="size-[18px]" />
|
|
18
|
+
</button>
|
|
19
|
+
<span class="flex size-[var(--ads-navbar-desk-avatar)] items-center justify-center overflow-hidden rounded-full bg-ads-neutro-claro-30" aria-hidden="true">
|
|
20
|
+
<img v-if="avatarUrl" :src="avatarUrl" alt="" class="size-full object-cover" />
|
|
21
|
+
<UIcon v-else name="i-lucide-user" class="size-4 text-[var(--ads-text-secondary)]" />
|
|
22
|
+
</span>
|
|
23
|
+
</div>
|
|
24
|
+
</header>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<script setup lang="ts">
|
|
28
|
+
import radarUrl from "../assets/brand/radar.svg?url";
|
|
29
|
+
import AdsBroadcast from "./AdsBroadcast.vue";
|
|
30
|
+
import AdsProductTag, { type AdsProductTagName } from "./AdsProductTag.vue";
|
|
31
|
+
|
|
32
|
+
export type AdsNavbarDeskAction = {
|
|
33
|
+
id: string;
|
|
34
|
+
label: string;
|
|
35
|
+
icon: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
withDefaults(
|
|
39
|
+
defineProps<{
|
|
40
|
+
product?: AdsProductTagName | null;
|
|
41
|
+
productLabel?: string;
|
|
42
|
+
showBroadcast?: boolean;
|
|
43
|
+
avatarUrl?: string;
|
|
44
|
+
actions?: AdsNavbarDeskAction[];
|
|
45
|
+
}>(),
|
|
46
|
+
{
|
|
47
|
+
product: "performance",
|
|
48
|
+
productLabel: "performance",
|
|
49
|
+
showBroadcast: true,
|
|
50
|
+
avatarUrl: "",
|
|
51
|
+
actions: () => [
|
|
52
|
+
{ id: "alerts", label: "Alertas", icon: "i-lucide-bell" },
|
|
53
|
+
{ id: "settings", label: "Configurações", icon: "i-lucide-settings" },
|
|
54
|
+
{ id: "apps", label: "Aplicativos", icon: "i-lucide-layout-grid" },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const emit = defineEmits<{
|
|
60
|
+
action: [id: string];
|
|
61
|
+
}>();
|
|
62
|
+
</script>
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<span
|
|
3
|
-
class="inline-flex h-[var(--ads-notability-height)] min-w-[var(--ads-notability-min-width)] shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full px-[var(--ads-notability-padding-x)] py-1 text-center text-[length:var(--ads-notability-font-size)] [font-weight:var(--ads-notability-weight)] leading-[1.2] whitespace-nowrap"
|
|
4
|
-
:class="surfaceClass"
|
|
5
|
-
>
|
|
6
|
-
{{ resolvedLabel }}
|
|
7
|
-
</span>
|
|
8
|
-
</template>
|
|
9
|
-
|
|
10
|
-
<script setup lang="ts">
|
|
11
|
-
import { computed } from "vue";
|
|
12
|
-
import { formatNotability } from "../utils/notability";
|
|
13
|
-
|
|
14
|
-
const props = defineProps<{
|
|
15
|
-
notability: string;
|
|
16
|
-
label?: string;
|
|
17
|
-
}>();
|
|
18
|
-
|
|
19
|
-
const surfaces: Record<string, string> = {
|
|
20
|
-
image: "bg-[var(--ads-status-new-100)] text-[var(--ads-status-new-800)]",
|
|
21
|
-
traffic: "bg-[var(--ads-status-success-100)] text-[var(--ads-status-success-800)]",
|
|
22
|
-
profit: "bg-[var(--ads-status-innovation-100)] text-[var(--ads-status-innovation-800)]",
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const surfaceClass = computed(() => surfaces[props.notability] ?? surfaces.image);
|
|
26
|
-
const resolvedLabel = computed(() => props.label ?? formatNotability(props.notability));
|
|
27
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
class="inline-flex h-[var(--ads-notability-height)] min-w-[var(--ads-notability-min-width)] shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full px-[var(--ads-notability-padding-x)] py-1 text-center text-[length:var(--ads-notability-font-size)] [font-weight:var(--ads-notability-weight)] leading-[1.2] whitespace-nowrap"
|
|
4
|
+
:class="surfaceClass"
|
|
5
|
+
>
|
|
6
|
+
{{ resolvedLabel }}
|
|
7
|
+
</span>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import { computed } from "vue";
|
|
12
|
+
import { formatNotability } from "../utils/notability";
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
notability: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const surfaces: Record<string, string> = {
|
|
20
|
+
image: "bg-[var(--ads-status-new-100)] text-[var(--ads-status-new-800)]",
|
|
21
|
+
traffic: "bg-[var(--ads-status-success-100)] text-[var(--ads-status-success-800)]",
|
|
22
|
+
profit: "bg-[var(--ads-status-innovation-100)] text-[var(--ads-status-innovation-800)]",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const surfaceClass = computed(() => surfaces[props.notability] ?? surfaces.image);
|
|
26
|
+
const resolvedLabel = computed(() => props.label ?? formatNotability(props.notability));
|
|
27
|
+
</script>
|
|
@@ -1,166 +1,166 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="flex min-w-0 flex-col gap-0.5">
|
|
3
|
-
<div
|
|
4
|
-
v-if="label || info"
|
|
5
|
-
class="flex items-center gap-0.5 text-[length:var(--ads-number-label-size)] leading-[1.2]"
|
|
6
|
-
:class="supportClass"
|
|
7
|
-
>
|
|
8
|
-
<label v-if="label" :for="fieldId">{{ label }}</label>
|
|
9
|
-
<InvertedTooltip v-if="info" :text="info">
|
|
10
|
-
<span class="inline-flex">
|
|
11
|
-
<UIcon name="i-lucide-info" class="size-3 text-xs" />
|
|
12
|
-
</span>
|
|
13
|
-
</InvertedTooltip>
|
|
14
|
-
</div>
|
|
15
|
-
|
|
16
|
-
<div
|
|
17
|
-
class="flex h-[var(--ads-number-height)] w-full transition-colors min-w-0 items-center gap-[var(--ads-number-gap)] rounded-[var(--ads-number-radius)] px-[var(--ads-number-padding-x)]"
|
|
18
|
-
:class="fieldClass"
|
|
19
|
-
:data-state="visualState"
|
|
20
|
-
>
|
|
21
|
-
<input
|
|
22
|
-
:id="fieldId"
|
|
23
|
-
ref="inputEl"
|
|
24
|
-
:value="displayValue"
|
|
25
|
-
type="number"
|
|
26
|
-
inputmode="decimal"
|
|
27
|
-
size="4"
|
|
28
|
-
class="w-0 min-w-0 flex-1 bg-transparent text-[length:var(--ads-number-font-size)] leading-[1.3] outline-none placeholder:text-current disabled:cursor-not-allowed [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
29
|
-
:placeholder="placeholder"
|
|
30
|
-
:disabled="isDisabled"
|
|
31
|
-
:min="min"
|
|
32
|
-
:step="step"
|
|
33
|
-
:aria-invalid="error ? true : undefined"
|
|
34
|
-
:aria-describedby="supportText ? supportId : undefined"
|
|
35
|
-
@input="onInput"
|
|
36
|
-
@focus="emit('focus', $event)"
|
|
37
|
-
@blur="emit('blur', $event)"
|
|
38
|
-
>
|
|
39
|
-
<div class="flex shrink-0 flex-col">
|
|
40
|
-
<button
|
|
41
|
-
type="button"
|
|
42
|
-
class="inline-flex size-4 items-center justify-center rounded-sm text-current hover:bg-[var(--ads-button-secondary-hover)] disabled:cursor-not-allowed"
|
|
43
|
-
aria-label="Aumentar valor"
|
|
44
|
-
:disabled="isDisabled"
|
|
45
|
-
@mousedown.prevent="stepBy(1)"
|
|
46
|
-
>
|
|
47
|
-
<UIcon name="i-lucide-chevron-up" class="size-3.5" />
|
|
48
|
-
</button>
|
|
49
|
-
<button
|
|
50
|
-
type="button"
|
|
51
|
-
class="inline-flex size-4 items-center justify-center rounded-sm text-current hover:bg-[var(--ads-button-secondary-hover)] disabled:cursor-not-allowed"
|
|
52
|
-
aria-label="Diminuir valor"
|
|
53
|
-
:disabled="isDisabled"
|
|
54
|
-
@mousedown.prevent="stepBy(-1)"
|
|
55
|
-
>
|
|
56
|
-
<UIcon name="i-lucide-chevron-down" class="size-3.5" />
|
|
57
|
-
</button>
|
|
58
|
-
</div>
|
|
59
|
-
<span v-if="suffix" class="shrink-0 text-[length:var(--ads-number-font-size)] leading-[1.3]">{{ suffix }}</span>
|
|
60
|
-
</div>
|
|
61
|
-
|
|
62
|
-
<p
|
|
63
|
-
v-if="supportText"
|
|
64
|
-
:id="supportId"
|
|
65
|
-
class="text-xs leading-[1.2]"
|
|
66
|
-
:class="supportClass"
|
|
67
|
-
>
|
|
68
|
-
{{ supportText }}
|
|
69
|
-
</p>
|
|
70
|
-
</div>
|
|
71
|
-
</template>
|
|
72
|
-
|
|
73
|
-
<script setup lang="ts">
|
|
74
|
-
import { computed, useId, useTemplateRef } from "vue";
|
|
75
|
-
import InvertedTooltip from "./InvertedTooltip.vue";
|
|
76
|
-
|
|
77
|
-
export type AdsNumberInputState = "default" | "active" | "focus" | "error" | "disabled";
|
|
78
|
-
|
|
79
|
-
const props = withDefaults(
|
|
80
|
-
defineProps<{
|
|
81
|
-
label?: string;
|
|
82
|
-
hint?: string;
|
|
83
|
-
info?: string;
|
|
84
|
-
error?: string;
|
|
85
|
-
placeholder?: string;
|
|
86
|
-
suffix?: string;
|
|
87
|
-
disabled?: boolean;
|
|
88
|
-
min?: string | number;
|
|
89
|
-
step?: string | number;
|
|
90
|
-
/** Estado congelado para a documentação. `null` segue o campo. */
|
|
91
|
-
state?: AdsNumberInputState | null;
|
|
92
|
-
}>(),
|
|
93
|
-
{
|
|
94
|
-
label: "",
|
|
95
|
-
hint: "",
|
|
96
|
-
info: "",
|
|
97
|
-
error: "",
|
|
98
|
-
placeholder: "",
|
|
99
|
-
suffix: "",
|
|
100
|
-
disabled: false,
|
|
101
|
-
state: null,
|
|
102
|
-
},
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
const model = defineModel<string | number>({ default: "" });
|
|
106
|
-
|
|
107
|
-
const emit = defineEmits<{
|
|
108
|
-
focus: [event: FocusEvent];
|
|
109
|
-
blur: [event: FocusEvent];
|
|
110
|
-
}>();
|
|
111
|
-
|
|
112
|
-
const fieldId = useId();
|
|
113
|
-
const supportId = useId();
|
|
114
|
-
const inputEl = useTemplateRef<HTMLInputElement>("inputEl");
|
|
115
|
-
|
|
116
|
-
const displayValue = computed(() =>
|
|
117
|
-
model.value === undefined || model.value === null ? "" : String(model.value),
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
const onInput = (event: Event) => {
|
|
121
|
-
model.value = (event.target as HTMLInputElement).value;
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const isDisabled = computed(() => props.disabled || props.state === "disabled");
|
|
125
|
-
|
|
126
|
-
const stepBy = (direction: 1 | -1) => {
|
|
127
|
-
const input = inputEl.value;
|
|
128
|
-
if (!input || isDisabled.value) return;
|
|
129
|
-
if (input.value === "") input.value = String(props.min ?? 0);
|
|
130
|
-
if (direction > 0) input.stepUp();
|
|
131
|
-
else input.stepDown();
|
|
132
|
-
model.value = input.value;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const visualState = computed<AdsNumberInputState>(() => {
|
|
136
|
-
if (isDisabled.value) return "disabled";
|
|
137
|
-
if (props.state) return props.state;
|
|
138
|
-
if (props.error) return "error";
|
|
139
|
-
return displayValue.value ? "active" : "default";
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
const supportText = computed(() => props.error || props.hint);
|
|
143
|
-
const supportClass = computed(() =>
|
|
144
|
-
visualState.value === "disabled"
|
|
145
|
-
? "text-[var(--ads-text-tertiary)]"
|
|
146
|
-
: "text-[var(--ads-text-secondary)]",
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
const fieldClass = computed(() => {
|
|
150
|
-
const interactiveFocus = props.state === null && !isDisabled.value && !props.error;
|
|
151
|
-
const focus = interactiveFocus
|
|
152
|
-
? "hover:not-focus-within:border-[var(--ads-number-hover-border)] focus-within:border-(length:--ads-border-width-slim) focus-within:border-[var(--ads-number-focus-border)]"
|
|
153
|
-
: "";
|
|
154
|
-
|
|
155
|
-
if (visualState.value === "disabled") {
|
|
156
|
-
return "border border-[var(--ads-number-disabled-border)] bg-[var(--ads-number-disabled-background)] text-[var(--ads-number-disabled-text)]";
|
|
157
|
-
}
|
|
158
|
-
if (visualState.value === "error") {
|
|
159
|
-
return `border-(length:--ads-border-width-slim) border-[var(--ads-number-error-border)] bg-[var(--ads-number-background)] text-[var(--ads-number-text)] ${focus}`;
|
|
160
|
-
}
|
|
161
|
-
if (visualState.value === "focus") {
|
|
162
|
-
return "border-(length:--ads-border-width-slim) border-[var(--ads-number-focus-border)] bg-[var(--ads-number-background)] text-[var(--ads-number-text)]";
|
|
163
|
-
}
|
|
164
|
-
return `border border-[var(--ads-number-border)] bg-[var(--ads-number-background)] text-[var(--ads-number-text)] ${focus}`;
|
|
165
|
-
});
|
|
166
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex min-w-0 flex-col gap-0.5">
|
|
3
|
+
<div
|
|
4
|
+
v-if="label || info"
|
|
5
|
+
class="flex items-center gap-0.5 text-[length:var(--ads-number-label-size)] leading-[1.2]"
|
|
6
|
+
:class="supportClass"
|
|
7
|
+
>
|
|
8
|
+
<label v-if="label" :for="fieldId">{{ label }}</label>
|
|
9
|
+
<InvertedTooltip v-if="info" :text="info">
|
|
10
|
+
<span class="inline-flex">
|
|
11
|
+
<UIcon name="i-lucide-info" class="size-3 text-xs" />
|
|
12
|
+
</span>
|
|
13
|
+
</InvertedTooltip>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div
|
|
17
|
+
class="flex h-[var(--ads-number-height)] w-full transition-colors min-w-0 items-center gap-[var(--ads-number-gap)] rounded-[var(--ads-number-radius)] px-[var(--ads-number-padding-x)]"
|
|
18
|
+
:class="fieldClass"
|
|
19
|
+
:data-state="visualState"
|
|
20
|
+
>
|
|
21
|
+
<input
|
|
22
|
+
:id="fieldId"
|
|
23
|
+
ref="inputEl"
|
|
24
|
+
:value="displayValue"
|
|
25
|
+
type="number"
|
|
26
|
+
inputmode="decimal"
|
|
27
|
+
size="4"
|
|
28
|
+
class="w-0 min-w-0 flex-1 bg-transparent text-[length:var(--ads-number-font-size)] leading-[1.3] outline-none placeholder:text-current disabled:cursor-not-allowed [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
|
29
|
+
:placeholder="placeholder"
|
|
30
|
+
:disabled="isDisabled"
|
|
31
|
+
:min="min"
|
|
32
|
+
:step="step"
|
|
33
|
+
:aria-invalid="error ? true : undefined"
|
|
34
|
+
:aria-describedby="supportText ? supportId : undefined"
|
|
35
|
+
@input="onInput"
|
|
36
|
+
@focus="emit('focus', $event)"
|
|
37
|
+
@blur="emit('blur', $event)"
|
|
38
|
+
>
|
|
39
|
+
<div class="flex shrink-0 flex-col">
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
class="inline-flex size-4 items-center justify-center rounded-sm text-current hover:bg-[var(--ads-button-secondary-hover)] disabled:cursor-not-allowed"
|
|
43
|
+
aria-label="Aumentar valor"
|
|
44
|
+
:disabled="isDisabled"
|
|
45
|
+
@mousedown.prevent="stepBy(1)"
|
|
46
|
+
>
|
|
47
|
+
<UIcon name="i-lucide-chevron-up" class="size-3.5" />
|
|
48
|
+
</button>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
class="inline-flex size-4 items-center justify-center rounded-sm text-current hover:bg-[var(--ads-button-secondary-hover)] disabled:cursor-not-allowed"
|
|
52
|
+
aria-label="Diminuir valor"
|
|
53
|
+
:disabled="isDisabled"
|
|
54
|
+
@mousedown.prevent="stepBy(-1)"
|
|
55
|
+
>
|
|
56
|
+
<UIcon name="i-lucide-chevron-down" class="size-3.5" />
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
<span v-if="suffix" class="shrink-0 text-[length:var(--ads-number-font-size)] leading-[1.3]">{{ suffix }}</span>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<p
|
|
63
|
+
v-if="supportText"
|
|
64
|
+
:id="supportId"
|
|
65
|
+
class="text-xs leading-[1.2]"
|
|
66
|
+
:class="supportClass"
|
|
67
|
+
>
|
|
68
|
+
{{ supportText }}
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
</template>
|
|
72
|
+
|
|
73
|
+
<script setup lang="ts">
|
|
74
|
+
import { computed, useId, useTemplateRef } from "vue";
|
|
75
|
+
import InvertedTooltip from "./InvertedTooltip.vue";
|
|
76
|
+
|
|
77
|
+
export type AdsNumberInputState = "default" | "active" | "focus" | "error" | "disabled";
|
|
78
|
+
|
|
79
|
+
const props = withDefaults(
|
|
80
|
+
defineProps<{
|
|
81
|
+
label?: string;
|
|
82
|
+
hint?: string;
|
|
83
|
+
info?: string;
|
|
84
|
+
error?: string;
|
|
85
|
+
placeholder?: string;
|
|
86
|
+
suffix?: string;
|
|
87
|
+
disabled?: boolean;
|
|
88
|
+
min?: string | number;
|
|
89
|
+
step?: string | number;
|
|
90
|
+
/** Estado congelado para a documentação. `null` segue o campo. */
|
|
91
|
+
state?: AdsNumberInputState | null;
|
|
92
|
+
}>(),
|
|
93
|
+
{
|
|
94
|
+
label: "",
|
|
95
|
+
hint: "",
|
|
96
|
+
info: "",
|
|
97
|
+
error: "",
|
|
98
|
+
placeholder: "",
|
|
99
|
+
suffix: "",
|
|
100
|
+
disabled: false,
|
|
101
|
+
state: null,
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const model = defineModel<string | number>({ default: "" });
|
|
106
|
+
|
|
107
|
+
const emit = defineEmits<{
|
|
108
|
+
focus: [event: FocusEvent];
|
|
109
|
+
blur: [event: FocusEvent];
|
|
110
|
+
}>();
|
|
111
|
+
|
|
112
|
+
const fieldId = useId();
|
|
113
|
+
const supportId = useId();
|
|
114
|
+
const inputEl = useTemplateRef<HTMLInputElement>("inputEl");
|
|
115
|
+
|
|
116
|
+
const displayValue = computed(() =>
|
|
117
|
+
model.value === undefined || model.value === null ? "" : String(model.value),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const onInput = (event: Event) => {
|
|
121
|
+
model.value = (event.target as HTMLInputElement).value;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const isDisabled = computed(() => props.disabled || props.state === "disabled");
|
|
125
|
+
|
|
126
|
+
const stepBy = (direction: 1 | -1) => {
|
|
127
|
+
const input = inputEl.value;
|
|
128
|
+
if (!input || isDisabled.value) return;
|
|
129
|
+
if (input.value === "") input.value = String(props.min ?? 0);
|
|
130
|
+
if (direction > 0) input.stepUp();
|
|
131
|
+
else input.stepDown();
|
|
132
|
+
model.value = input.value;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const visualState = computed<AdsNumberInputState>(() => {
|
|
136
|
+
if (isDisabled.value) return "disabled";
|
|
137
|
+
if (props.state) return props.state;
|
|
138
|
+
if (props.error) return "error";
|
|
139
|
+
return displayValue.value ? "active" : "default";
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const supportText = computed(() => props.error || props.hint);
|
|
143
|
+
const supportClass = computed(() =>
|
|
144
|
+
visualState.value === "disabled"
|
|
145
|
+
? "text-[var(--ads-text-tertiary)]"
|
|
146
|
+
: "text-[var(--ads-text-secondary)]",
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const fieldClass = computed(() => {
|
|
150
|
+
const interactiveFocus = props.state === null && !isDisabled.value && !props.error;
|
|
151
|
+
const focus = interactiveFocus
|
|
152
|
+
? "hover:not-focus-within:border-[var(--ads-number-hover-border)] focus-within:border-(length:--ads-border-width-slim) focus-within:border-[var(--ads-number-focus-border)]"
|
|
153
|
+
: "";
|
|
154
|
+
|
|
155
|
+
if (visualState.value === "disabled") {
|
|
156
|
+
return "border border-[var(--ads-number-disabled-border)] bg-[var(--ads-number-disabled-background)] text-[var(--ads-number-disabled-text)]";
|
|
157
|
+
}
|
|
158
|
+
if (visualState.value === "error") {
|
|
159
|
+
return `border-(length:--ads-border-width-slim) border-[var(--ads-number-error-border)] bg-[var(--ads-number-background)] text-[var(--ads-number-text)] ${focus}`;
|
|
160
|
+
}
|
|
161
|
+
if (visualState.value === "focus") {
|
|
162
|
+
return "border-(length:--ads-border-width-slim) border-[var(--ads-number-focus-border)] bg-[var(--ads-number-background)] text-[var(--ads-number-text)]";
|
|
163
|
+
}
|
|
164
|
+
return `border border-[var(--ads-number-border)] bg-[var(--ads-number-background)] text-[var(--ads-number-text)] ${focus}`;
|
|
165
|
+
});
|
|
166
|
+
</script>
|