ads-nuxt 0.1.0 → 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 -1
- 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,143 +1,143 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="flex w-full min-w-0 flex-col gap-0.5">
|
|
3
|
-
<div
|
|
4
|
-
v-if="label"
|
|
5
|
-
class="flex items-center gap-0.5 text-[length:var(--ads-input-label-size)] leading-[1.2]"
|
|
6
|
-
:class="supportClass"
|
|
7
|
-
>
|
|
8
|
-
<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-input-height)] items-center transition-colors gap-[var(--ads-input-gap)] rounded-[var(--ads-input-radius)] px-[var(--ads-input-padding-x)]"
|
|
18
|
-
:class="fieldClass"
|
|
19
|
-
:data-state="visualState"
|
|
20
|
-
>
|
|
21
|
-
<input
|
|
22
|
-
:id="fieldId"
|
|
23
|
-
:value="displayValue"
|
|
24
|
-
:type="type"
|
|
25
|
-
class="min-w-0 flex-1 bg-transparent text-[length:var(--ads-input-font-size)] leading-[1.3] outline-none placeholder:text-current disabled:cursor-not-allowed"
|
|
26
|
-
:class="type === 'number' ? '[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none' : undefined"
|
|
27
|
-
:placeholder="placeholder"
|
|
28
|
-
:disabled="isDisabled"
|
|
29
|
-
:min="min"
|
|
30
|
-
:step="step"
|
|
31
|
-
:aria-invalid="error ? true : undefined"
|
|
32
|
-
:aria-describedby="supportText ? supportId : undefined"
|
|
33
|
-
@input="onInput"
|
|
34
|
-
@focus="emit('focus', $event)"
|
|
35
|
-
@blur="emit('blur', $event)"
|
|
36
|
-
>
|
|
37
|
-
<span v-if="suffix" class="shrink-0 text-[length:var(--ads-input-font-size)] leading-[1.3]">{{ suffix }}</span>
|
|
38
|
-
<UIcon v-if="icon" :name="icon" class="size-4 shrink-0 text-base" />
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
<p
|
|
42
|
-
v-if="supportText"
|
|
43
|
-
:id="supportId"
|
|
44
|
-
class="text-xs leading-[1.2]"
|
|
45
|
-
:class="supportClass"
|
|
46
|
-
>
|
|
47
|
-
{{ supportText }}
|
|
48
|
-
</p>
|
|
49
|
-
</div>
|
|
50
|
-
</template>
|
|
51
|
-
|
|
52
|
-
<script setup lang="ts">
|
|
53
|
-
import { computed, useId } from "vue";
|
|
54
|
-
import InvertedTooltip from "./InvertedTooltip.vue";
|
|
55
|
-
|
|
56
|
-
export type AdsInputState = "default" | "active" | "focus" | "error" | "disabled" | "ai";
|
|
57
|
-
|
|
58
|
-
const props = withDefaults(
|
|
59
|
-
defineProps<{
|
|
60
|
-
label?: string;
|
|
61
|
-
hint?: string;
|
|
62
|
-
error?: string;
|
|
63
|
-
info?: string;
|
|
64
|
-
placeholder?: string;
|
|
65
|
-
disabled?: boolean;
|
|
66
|
-
icon?: string;
|
|
67
|
-
type?: "text" | "number";
|
|
68
|
-
suffix?: string;
|
|
69
|
-
min?: string | number;
|
|
70
|
-
step?: string | number;
|
|
71
|
-
/** Estado congelado para a documentação. `null` segue o campo. */
|
|
72
|
-
state?: AdsInputState | null;
|
|
73
|
-
}>(),
|
|
74
|
-
{
|
|
75
|
-
label: "",
|
|
76
|
-
hint: "",
|
|
77
|
-
error: "",
|
|
78
|
-
info: "",
|
|
79
|
-
placeholder: "",
|
|
80
|
-
disabled: false,
|
|
81
|
-
icon: "",
|
|
82
|
-
type: "text",
|
|
83
|
-
suffix: "",
|
|
84
|
-
state: null,
|
|
85
|
-
},
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const model = defineModel<string | number>({ default: "" });
|
|
89
|
-
|
|
90
|
-
const emit = defineEmits<{
|
|
91
|
-
focus: [event: FocusEvent];
|
|
92
|
-
blur: [event: FocusEvent];
|
|
93
|
-
}>();
|
|
94
|
-
|
|
95
|
-
const fieldId = useId();
|
|
96
|
-
const supportId = useId();
|
|
97
|
-
|
|
98
|
-
const displayValue = computed(() =>
|
|
99
|
-
model.value === undefined || model.value === null ? "" : String(model.value),
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const onInput = (event: Event) => {
|
|
103
|
-
const value = (event.target as HTMLInputElement).value;
|
|
104
|
-
model.value = value;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
const isDisabled = computed(() => props.disabled || props.state === "disabled");
|
|
108
|
-
|
|
109
|
-
const visualState = computed<AdsInputState>(() => {
|
|
110
|
-
if (isDisabled.value) return "disabled";
|
|
111
|
-
if (props.state) return props.state;
|
|
112
|
-
if (props.error) return "error";
|
|
113
|
-
return displayValue.value ? "active" : "default";
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
const supportText = computed(() => props.error || props.hint);
|
|
117
|
-
const supportClass = computed(() =>
|
|
118
|
-
visualState.value === "disabled"
|
|
119
|
-
? "text-[var(--ads-text-tertiary)]"
|
|
120
|
-
: "text-[var(--ads-text-secondary)]",
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
const fieldClass = computed(() => {
|
|
124
|
-
const interactiveFocus = props.state === null && !isDisabled.value && !props.error;
|
|
125
|
-
const focus = interactiveFocus
|
|
126
|
-
? "hover:not-focus-within:border-[var(--ads-input-hover-border)] focus-within:border-(length:--ads-border-width-slim) focus-within:border-[var(--ads-input-focus-border)]"
|
|
127
|
-
: "";
|
|
128
|
-
|
|
129
|
-
if (visualState.value === "disabled") {
|
|
130
|
-
return "border border-[var(--ads-input-disabled-border)] bg-[var(--ads-input-disabled-background)] text-[var(--ads-input-disabled-text)]";
|
|
131
|
-
}
|
|
132
|
-
if (visualState.value === "error") {
|
|
133
|
-
return `border-(length:--ads-border-width-slim) border-[var(--ads-input-error-border)] bg-[var(--ads-input-background)] text-[var(--ads-input-text)] ${focus}`;
|
|
134
|
-
}
|
|
135
|
-
if (visualState.value === "focus") {
|
|
136
|
-
return "border-(length:--ads-border-width-slim) border-[var(--ads-input-focus-border)] bg-[var(--ads-input-background)] text-[var(--ads-input-text)]";
|
|
137
|
-
}
|
|
138
|
-
if (visualState.value === "ai") {
|
|
139
|
-
return "border border-[#7358cc] bg-[#f3f0ff] text-[var(--ads-text-secondary)]";
|
|
140
|
-
}
|
|
141
|
-
return `border border-[var(--ads-input-border)] bg-[var(--ads-input-background)] text-[var(--ads-input-text)] ${focus}`;
|
|
142
|
-
});
|
|
143
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex w-full min-w-0 flex-col gap-0.5">
|
|
3
|
+
<div
|
|
4
|
+
v-if="label"
|
|
5
|
+
class="flex items-center gap-0.5 text-[length:var(--ads-input-label-size)] leading-[1.2]"
|
|
6
|
+
:class="supportClass"
|
|
7
|
+
>
|
|
8
|
+
<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-input-height)] items-center transition-colors gap-[var(--ads-input-gap)] rounded-[var(--ads-input-radius)] px-[var(--ads-input-padding-x)]"
|
|
18
|
+
:class="fieldClass"
|
|
19
|
+
:data-state="visualState"
|
|
20
|
+
>
|
|
21
|
+
<input
|
|
22
|
+
:id="fieldId"
|
|
23
|
+
:value="displayValue"
|
|
24
|
+
:type="type"
|
|
25
|
+
class="min-w-0 flex-1 bg-transparent text-[length:var(--ads-input-font-size)] leading-[1.3] outline-none placeholder:text-current disabled:cursor-not-allowed"
|
|
26
|
+
:class="type === 'number' ? '[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none' : undefined"
|
|
27
|
+
:placeholder="placeholder"
|
|
28
|
+
:disabled="isDisabled"
|
|
29
|
+
:min="min"
|
|
30
|
+
:step="step"
|
|
31
|
+
:aria-invalid="error ? true : undefined"
|
|
32
|
+
:aria-describedby="supportText ? supportId : undefined"
|
|
33
|
+
@input="onInput"
|
|
34
|
+
@focus="emit('focus', $event)"
|
|
35
|
+
@blur="emit('blur', $event)"
|
|
36
|
+
>
|
|
37
|
+
<span v-if="suffix" class="shrink-0 text-[length:var(--ads-input-font-size)] leading-[1.3]">{{ suffix }}</span>
|
|
38
|
+
<UIcon v-if="icon" :name="icon" class="size-4 shrink-0 text-base" />
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<p
|
|
42
|
+
v-if="supportText"
|
|
43
|
+
:id="supportId"
|
|
44
|
+
class="text-xs leading-[1.2]"
|
|
45
|
+
:class="supportClass"
|
|
46
|
+
>
|
|
47
|
+
{{ supportText }}
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script setup lang="ts">
|
|
53
|
+
import { computed, useId } from "vue";
|
|
54
|
+
import InvertedTooltip from "./InvertedTooltip.vue";
|
|
55
|
+
|
|
56
|
+
export type AdsInputState = "default" | "active" | "focus" | "error" | "disabled" | "ai";
|
|
57
|
+
|
|
58
|
+
const props = withDefaults(
|
|
59
|
+
defineProps<{
|
|
60
|
+
label?: string;
|
|
61
|
+
hint?: string;
|
|
62
|
+
error?: string;
|
|
63
|
+
info?: string;
|
|
64
|
+
placeholder?: string;
|
|
65
|
+
disabled?: boolean;
|
|
66
|
+
icon?: string;
|
|
67
|
+
type?: "text" | "number";
|
|
68
|
+
suffix?: string;
|
|
69
|
+
min?: string | number;
|
|
70
|
+
step?: string | number;
|
|
71
|
+
/** Estado congelado para a documentação. `null` segue o campo. */
|
|
72
|
+
state?: AdsInputState | null;
|
|
73
|
+
}>(),
|
|
74
|
+
{
|
|
75
|
+
label: "",
|
|
76
|
+
hint: "",
|
|
77
|
+
error: "",
|
|
78
|
+
info: "",
|
|
79
|
+
placeholder: "",
|
|
80
|
+
disabled: false,
|
|
81
|
+
icon: "",
|
|
82
|
+
type: "text",
|
|
83
|
+
suffix: "",
|
|
84
|
+
state: null,
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const model = defineModel<string | number>({ default: "" });
|
|
89
|
+
|
|
90
|
+
const emit = defineEmits<{
|
|
91
|
+
focus: [event: FocusEvent];
|
|
92
|
+
blur: [event: FocusEvent];
|
|
93
|
+
}>();
|
|
94
|
+
|
|
95
|
+
const fieldId = useId();
|
|
96
|
+
const supportId = useId();
|
|
97
|
+
|
|
98
|
+
const displayValue = computed(() =>
|
|
99
|
+
model.value === undefined || model.value === null ? "" : String(model.value),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const onInput = (event: Event) => {
|
|
103
|
+
const value = (event.target as HTMLInputElement).value;
|
|
104
|
+
model.value = value;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const isDisabled = computed(() => props.disabled || props.state === "disabled");
|
|
108
|
+
|
|
109
|
+
const visualState = computed<AdsInputState>(() => {
|
|
110
|
+
if (isDisabled.value) return "disabled";
|
|
111
|
+
if (props.state) return props.state;
|
|
112
|
+
if (props.error) return "error";
|
|
113
|
+
return displayValue.value ? "active" : "default";
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const supportText = computed(() => props.error || props.hint);
|
|
117
|
+
const supportClass = computed(() =>
|
|
118
|
+
visualState.value === "disabled"
|
|
119
|
+
? "text-[var(--ads-text-tertiary)]"
|
|
120
|
+
: "text-[var(--ads-text-secondary)]",
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const fieldClass = computed(() => {
|
|
124
|
+
const interactiveFocus = props.state === null && !isDisabled.value && !props.error;
|
|
125
|
+
const focus = interactiveFocus
|
|
126
|
+
? "hover:not-focus-within:border-[var(--ads-input-hover-border)] focus-within:border-(length:--ads-border-width-slim) focus-within:border-[var(--ads-input-focus-border)]"
|
|
127
|
+
: "";
|
|
128
|
+
|
|
129
|
+
if (visualState.value === "disabled") {
|
|
130
|
+
return "border border-[var(--ads-input-disabled-border)] bg-[var(--ads-input-disabled-background)] text-[var(--ads-input-disabled-text)]";
|
|
131
|
+
}
|
|
132
|
+
if (visualState.value === "error") {
|
|
133
|
+
return `border-(length:--ads-border-width-slim) border-[var(--ads-input-error-border)] bg-[var(--ads-input-background)] text-[var(--ads-input-text)] ${focus}`;
|
|
134
|
+
}
|
|
135
|
+
if (visualState.value === "focus") {
|
|
136
|
+
return "border-(length:--ads-border-width-slim) border-[var(--ads-input-focus-border)] bg-[var(--ads-input-background)] text-[var(--ads-input-text)]";
|
|
137
|
+
}
|
|
138
|
+
if (visualState.value === "ai") {
|
|
139
|
+
return "border border-[#7358cc] bg-[#f3f0ff] text-[var(--ads-text-secondary)]";
|
|
140
|
+
}
|
|
141
|
+
return `border border-[var(--ads-input-border)] bg-[var(--ads-input-background)] text-[var(--ads-input-text)] ${focus}`;
|
|
142
|
+
});
|
|
143
|
+
</script>
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<AdsFilterDropdown
|
|
3
|
-
v-model="model"
|
|
4
|
-
v-model:query="query"
|
|
5
|
-
type="radio"
|
|
6
|
-
:options="options"
|
|
7
|
-
:searchable="search"
|
|
8
|
-
:select-all="false"
|
|
9
|
-
:empty-label="emptyLabel"
|
|
10
|
-
:search-placeholder="searchPlaceholder"
|
|
11
|
-
:list-label="listLabel"
|
|
12
|
-
width-class="min-w-[var(--ads-list-min-width)] w-full"
|
|
13
|
-
@pick="emit('pick', model)"
|
|
14
|
-
/>
|
|
15
|
-
</template>
|
|
16
|
-
|
|
17
|
-
<script setup lang="ts">
|
|
18
|
-
import { ref } from "vue";
|
|
19
|
-
import AdsFilterDropdown from "./AdsFilterDropdown.vue";
|
|
20
|
-
import type { AdsFilterOption } from "../utils/ads-filter-select";
|
|
21
|
-
|
|
22
|
-
withDefaults(
|
|
23
|
-
defineProps<{
|
|
24
|
-
options: AdsFilterOption[];
|
|
25
|
-
/** Figma `Search`. */
|
|
26
|
-
search?: boolean;
|
|
27
|
-
emptyLabel?: string;
|
|
28
|
-
searchPlaceholder?: string;
|
|
29
|
-
listLabel?: string;
|
|
30
|
-
}>(),
|
|
31
|
-
{
|
|
32
|
-
search: true,
|
|
33
|
-
emptyLabel: "Nenhum resultado",
|
|
34
|
-
searchPlaceholder: "Pesquisar",
|
|
35
|
-
listLabel: "Opções",
|
|
36
|
-
},
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
const model = defineModel<string>({ default: "" });
|
|
40
|
-
const query = ref("");
|
|
41
|
-
const emit = defineEmits<{
|
|
42
|
-
pick: [value: string];
|
|
43
|
-
}>();
|
|
44
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<AdsFilterDropdown
|
|
3
|
+
v-model="model"
|
|
4
|
+
v-model:query="query"
|
|
5
|
+
type="radio"
|
|
6
|
+
:options="options"
|
|
7
|
+
:searchable="search"
|
|
8
|
+
:select-all="false"
|
|
9
|
+
:empty-label="emptyLabel"
|
|
10
|
+
:search-placeholder="searchPlaceholder"
|
|
11
|
+
:list-label="listLabel"
|
|
12
|
+
width-class="min-w-[var(--ads-list-min-width)] w-full"
|
|
13
|
+
@pick="emit('pick', model)"
|
|
14
|
+
/>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { ref } from "vue";
|
|
19
|
+
import AdsFilterDropdown from "./AdsFilterDropdown.vue";
|
|
20
|
+
import type { AdsFilterOption } from "../utils/ads-filter-select";
|
|
21
|
+
|
|
22
|
+
withDefaults(
|
|
23
|
+
defineProps<{
|
|
24
|
+
options: AdsFilterOption[];
|
|
25
|
+
/** Figma `Search`. */
|
|
26
|
+
search?: boolean;
|
|
27
|
+
emptyLabel?: string;
|
|
28
|
+
searchPlaceholder?: string;
|
|
29
|
+
listLabel?: string;
|
|
30
|
+
}>(),
|
|
31
|
+
{
|
|
32
|
+
search: true,
|
|
33
|
+
emptyLabel: "Nenhum resultado",
|
|
34
|
+
searchPlaceholder: "Pesquisar",
|
|
35
|
+
listLabel: "Opções",
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const model = defineModel<string>({ default: "" });
|
|
40
|
+
const query = ref("");
|
|
41
|
+
const emit = defineEmits<{
|
|
42
|
+
pick: [value: string];
|
|
43
|
+
}>();
|
|
44
|
+
</script>
|
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div
|
|
3
|
-
v-if="variant === 'shimmer'"
|
|
4
|
-
class="ads-loading-shimmer pointer-events-none absolute inset-0 z-10 overflow-hidden"
|
|
5
|
-
aria-hidden="true"
|
|
6
|
-
/>
|
|
7
|
-
|
|
8
|
-
<span
|
|
9
|
-
v-else-if="variant === 'skeleton' && lines < 1"
|
|
10
|
-
class="block animate-pulse rounded-[var(--ads-loading-radius)] bg-[var(--ads-loading-skeleton)] motion-reduce:animate-none"
|
|
11
|
-
role="status"
|
|
12
|
-
aria-label="Carregando"
|
|
13
|
-
/>
|
|
14
|
-
|
|
15
|
-
<div
|
|
16
|
-
v-else-if="variant === 'skeleton'"
|
|
17
|
-
class="flex flex-col gap-2"
|
|
18
|
-
role="status"
|
|
19
|
-
aria-label="Carregando"
|
|
20
|
-
>
|
|
21
|
-
<span
|
|
22
|
-
v-for="(width, index) in skeletonLines"
|
|
23
|
-
:key="index"
|
|
24
|
-
class="block h-[var(--ads-loading-bar)] animate-pulse rounded-[var(--ads-loading-radius)] bg-[var(--ads-loading-skeleton)] motion-reduce:animate-none"
|
|
25
|
-
:class="width"
|
|
26
|
-
aria-hidden="true"
|
|
27
|
-
/>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<div
|
|
31
|
-
v-else
|
|
32
|
-
role="status"
|
|
33
|
-
:aria-label="label ? undefined : 'Carregando'"
|
|
34
|
-
:class="spinnerRootClass"
|
|
35
|
-
>
|
|
36
|
-
<UIcon
|
|
37
|
-
name="i-lucide-loader-circle"
|
|
38
|
-
class="animate-spin motion-reduce:animate-none"
|
|
39
|
-
:class="[sizeClass, toneClass]"
|
|
40
|
-
aria-hidden="true"
|
|
41
|
-
/>
|
|
42
|
-
<p v-if="label" class="text-[length:var(--ads-loading-label-size)] text-[var(--ads-text-secondary)]">
|
|
43
|
-
{{ label }}
|
|
44
|
-
</p>
|
|
45
|
-
</div>
|
|
46
|
-
</template>
|
|
47
|
-
|
|
48
|
-
<script setup lang="ts">
|
|
49
|
-
import { computed } from "vue";
|
|
50
|
-
|
|
51
|
-
export type AdsLoadingVariant = "spinner" | "skeleton" | "shimmer";
|
|
52
|
-
export type AdsLoadingSize = "sm" | "md" | "lg";
|
|
53
|
-
export type AdsLoadingTone = "primary" | "muted";
|
|
54
|
-
|
|
55
|
-
const props = withDefaults(
|
|
56
|
-
defineProps<{
|
|
57
|
-
/**
|
|
58
|
-
* spinner: área ainda vazia.
|
|
59
|
-
* skeleton: o formato da tela já é conhecido.
|
|
60
|
-
* shimmer: o conteúdo continua visível, por baixo.
|
|
61
|
-
*/
|
|
62
|
-
variant?: AdsLoadingVariant;
|
|
63
|
-
/** sm na tabela, md na seção, lg no modal. */
|
|
64
|
-
size?: AdsLoadingSize;
|
|
65
|
-
/** primary no dado da tela, muted numa tabela secundária. */
|
|
66
|
-
tone?: AdsLoadingTone;
|
|
67
|
-
/** Centraliza o spinner numa seção vazia. */
|
|
68
|
-
region?: boolean;
|
|
69
|
-
label?: string;
|
|
70
|
-
/**
|
|
71
|
-
* Linhas do skeleton. Zero desenha um bloco só;
|
|
72
|
-
* altura e largura vêm da class.
|
|
73
|
-
*/
|
|
74
|
-
lines?: number;
|
|
75
|
-
}>(),
|
|
76
|
-
{
|
|
77
|
-
variant: "spinner",
|
|
78
|
-
size: "md",
|
|
79
|
-
tone: "primary",
|
|
80
|
-
region: false,
|
|
81
|
-
label: "",
|
|
82
|
-
lines: 0,
|
|
83
|
-
},
|
|
84
|
-
);
|
|
85
|
-
|
|
86
|
-
const skeletonWidths = ["w-2/3", "w-full", "w-1/2"] as const;
|
|
87
|
-
|
|
88
|
-
const skeletonLines = computed(() =>
|
|
89
|
-
Array.from({ length: props.lines }, (_, index) => skeletonWidths[index % skeletonWidths.length]),
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
const sizeClass = computed(() => {
|
|
93
|
-
if (props.size === "sm") return "size-6 shrink-0 text-[1.5rem]";
|
|
94
|
-
if (props.size === "lg") return "size-12 shrink-0 text-[3rem]";
|
|
95
|
-
return "size-[var(--ads-loading-spinner)] shrink-0 text-[length:var(--ads-loading-spinner)]";
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const toneClass = computed(() =>
|
|
99
|
-
props.tone === "muted" ? "text-muted" : "text-[var(--ads-loading-spinner-color)]",
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
const spinnerRootClass = computed(() => {
|
|
103
|
-
if (props.region) {
|
|
104
|
-
return "flex min-h-48 flex-col items-center justify-center gap-3 py-10 text-center sm:py-12";
|
|
105
|
-
}
|
|
106
|
-
if (props.label) return "inline-flex flex-col items-center gap-3 text-center";
|
|
107
|
-
return "inline-flex";
|
|
108
|
-
});
|
|
109
|
-
</script>
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
v-if="variant === 'shimmer'"
|
|
4
|
+
class="ads-loading-shimmer pointer-events-none absolute inset-0 z-10 overflow-hidden"
|
|
5
|
+
aria-hidden="true"
|
|
6
|
+
/>
|
|
7
|
+
|
|
8
|
+
<span
|
|
9
|
+
v-else-if="variant === 'skeleton' && lines < 1"
|
|
10
|
+
class="block animate-pulse rounded-[var(--ads-loading-radius)] bg-[var(--ads-loading-skeleton)] motion-reduce:animate-none"
|
|
11
|
+
role="status"
|
|
12
|
+
aria-label="Carregando"
|
|
13
|
+
/>
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
v-else-if="variant === 'skeleton'"
|
|
17
|
+
class="flex flex-col gap-2"
|
|
18
|
+
role="status"
|
|
19
|
+
aria-label="Carregando"
|
|
20
|
+
>
|
|
21
|
+
<span
|
|
22
|
+
v-for="(width, index) in skeletonLines"
|
|
23
|
+
:key="index"
|
|
24
|
+
class="block h-[var(--ads-loading-bar)] animate-pulse rounded-[var(--ads-loading-radius)] bg-[var(--ads-loading-skeleton)] motion-reduce:animate-none"
|
|
25
|
+
:class="width"
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div
|
|
31
|
+
v-else
|
|
32
|
+
role="status"
|
|
33
|
+
:aria-label="label ? undefined : 'Carregando'"
|
|
34
|
+
:class="spinnerRootClass"
|
|
35
|
+
>
|
|
36
|
+
<UIcon
|
|
37
|
+
name="i-lucide-loader-circle"
|
|
38
|
+
class="animate-spin motion-reduce:animate-none"
|
|
39
|
+
:class="[sizeClass, toneClass]"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
/>
|
|
42
|
+
<p v-if="label" class="text-[length:var(--ads-loading-label-size)] text-[var(--ads-text-secondary)]">
|
|
43
|
+
{{ label }}
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script setup lang="ts">
|
|
49
|
+
import { computed } from "vue";
|
|
50
|
+
|
|
51
|
+
export type AdsLoadingVariant = "spinner" | "skeleton" | "shimmer";
|
|
52
|
+
export type AdsLoadingSize = "sm" | "md" | "lg";
|
|
53
|
+
export type AdsLoadingTone = "primary" | "muted";
|
|
54
|
+
|
|
55
|
+
const props = withDefaults(
|
|
56
|
+
defineProps<{
|
|
57
|
+
/**
|
|
58
|
+
* spinner: área ainda vazia.
|
|
59
|
+
* skeleton: o formato da tela já é conhecido.
|
|
60
|
+
* shimmer: o conteúdo continua visível, por baixo.
|
|
61
|
+
*/
|
|
62
|
+
variant?: AdsLoadingVariant;
|
|
63
|
+
/** sm na tabela, md na seção, lg no modal. */
|
|
64
|
+
size?: AdsLoadingSize;
|
|
65
|
+
/** primary no dado da tela, muted numa tabela secundária. */
|
|
66
|
+
tone?: AdsLoadingTone;
|
|
67
|
+
/** Centraliza o spinner numa seção vazia. */
|
|
68
|
+
region?: boolean;
|
|
69
|
+
label?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Linhas do skeleton. Zero desenha um bloco só;
|
|
72
|
+
* altura e largura vêm da class.
|
|
73
|
+
*/
|
|
74
|
+
lines?: number;
|
|
75
|
+
}>(),
|
|
76
|
+
{
|
|
77
|
+
variant: "spinner",
|
|
78
|
+
size: "md",
|
|
79
|
+
tone: "primary",
|
|
80
|
+
region: false,
|
|
81
|
+
label: "",
|
|
82
|
+
lines: 0,
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const skeletonWidths = ["w-2/3", "w-full", "w-1/2"] as const;
|
|
87
|
+
|
|
88
|
+
const skeletonLines = computed(() =>
|
|
89
|
+
Array.from({ length: props.lines }, (_, index) => skeletonWidths[index % skeletonWidths.length]),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const sizeClass = computed(() => {
|
|
93
|
+
if (props.size === "sm") return "size-6 shrink-0 text-[1.5rem]";
|
|
94
|
+
if (props.size === "lg") return "size-12 shrink-0 text-[3rem]";
|
|
95
|
+
return "size-[var(--ads-loading-spinner)] shrink-0 text-[length:var(--ads-loading-spinner)]";
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const toneClass = computed(() =>
|
|
99
|
+
props.tone === "muted" ? "text-muted" : "text-[var(--ads-loading-spinner-color)]",
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const spinnerRootClass = computed(() => {
|
|
103
|
+
if (props.region) {
|
|
104
|
+
return "flex min-h-48 flex-col items-center justify-center gap-3 py-10 text-center sm:py-12";
|
|
105
|
+
}
|
|
106
|
+
if (props.label) return "inline-flex flex-col items-center gap-3 text-center";
|
|
107
|
+
return "inline-flex";
|
|
108
|
+
});
|
|
109
|
+
</script>
|