@dnax/ui 0.0.1
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/README.md +15 -0
- package/components/QAccordion.vue +68 -0
- package/components/QAccordionContent.vue +62 -0
- package/components/QAccordionItem.vue +24 -0
- package/components/QAccordionTrigger.vue +51 -0
- package/components/QActionSheet.vue +159 -0
- package/components/QApp.vue +8 -0
- package/components/QAutocomplete.vue +316 -0
- package/components/QAvatar.vue +92 -0
- package/components/QBackHeader.vue +69 -0
- package/components/QBackTop.vue +125 -0
- package/components/QBadge.vue +55 -0
- package/components/QBottomSheet.vue +177 -0
- package/components/QBottomSheetFooter.vue +9 -0
- package/components/QBottomSheetHeader.vue +35 -0
- package/components/QBottomSheetProvider.vue +62 -0
- package/components/QBottomSheetTrigger.vue +18 -0
- package/components/QBtn.vue +144 -0
- package/components/QBtnGroup.vue +31 -0
- package/components/QBubble.vue +36 -0
- package/components/QBubbleContent.vue +28 -0
- package/components/QBubbleGroup.vue +25 -0
- package/components/QBubbleReactions.vue +40 -0
- package/components/QCard.vue +63 -0
- package/components/QCardActions.vue +27 -0
- package/components/QCardContent.vue +29 -0
- package/components/QCarousel.vue +115 -0
- package/components/QCarouselContent.vue +23 -0
- package/components/QCarouselItem.vue +20 -0
- package/components/QCarouselNav.vue +51 -0
- package/components/QCheckbox.vue +146 -0
- package/components/QChip.vue +91 -0
- package/components/QCircularProgress.vue +122 -0
- package/components/QCol.vue +52 -0
- package/components/QCollapse.vue +141 -0
- package/components/QConfigProvider.vue +164 -0
- package/components/QContainer.vue +201 -0
- package/components/QCountDown.vue +164 -0
- package/components/QCountryPicker.vue +593 -0
- package/components/QDataGrid.vue +491 -0
- package/components/QDatePicker.vue +250 -0
- package/components/QDialog.vue +163 -0
- package/components/QDialogFooter.vue +9 -0
- package/components/QDialogHeader.vue +41 -0
- package/components/QDialogProvider.vue +65 -0
- package/components/QDialogTrigger.vue +18 -0
- package/components/QFab.vue +80 -0
- package/components/QFabAction.vue +48 -0
- package/components/QFilePicker.vue +294 -0
- package/components/QFooter.vue +72 -0
- package/components/QGallery.vue +280 -0
- package/components/QGrid.vue +54 -0
- package/components/QHeader.vue +38 -0
- package/components/QIcon.vue +53 -0
- package/components/QImagePicker.vue +248 -0
- package/components/QImagePreview.vue +225 -0
- package/components/QImagePreviewProvider.vue +35 -0
- package/components/QImg.vue +142 -0
- package/components/QInfiniteScroll.vue +109 -0
- package/components/QInnerLoading.vue +67 -0
- package/components/QInput.vue +289 -0
- package/components/QInputOtp.vue +238 -0
- package/components/QInputPassword.vue +67 -0
- package/components/QInputTag.vue +281 -0
- package/components/QIntersection.vue +119 -0
- package/components/QItem.vue +60 -0
- package/components/QItemSection.vue +47 -0
- package/components/QLinearProgress.vue +94 -0
- package/components/QList.vue +39 -0
- package/components/QLoading.vue +77 -0
- package/components/QLoadingProvider.vue +41 -0
- package/components/QMarquee.vue +56 -0
- package/components/QMessageScroller.vue +10 -0
- package/components/QMessageScrollerButton.vue +48 -0
- package/components/QMessageScrollerContent.vue +31 -0
- package/components/QMessageScrollerItem.vue +49 -0
- package/components/QMessageScrollerProvider.vue +198 -0
- package/components/QMessageScrollerViewport.vue +109 -0
- package/components/QNavMenu.vue +75 -0
- package/components/QNavMenuContent.vue +10 -0
- package/components/QNavMenuItem.vue +60 -0
- package/components/QNavMenuTrigger.vue +96 -0
- package/components/QNotifyProvider.vue +33 -0
- package/components/QNotifyToast.vue +60 -0
- package/components/QPage.vue +56 -0
- package/components/QPagination.vue +195 -0
- package/components/QParallax.vue +124 -0
- package/components/QPullToRefresh.vue +138 -0
- package/components/QRadio.vue +99 -0
- package/components/QRating.vue +105 -0
- package/components/QReorder.vue +191 -0
- package/components/QRollingText.vue +225 -0
- package/components/QRow.vue +23 -0
- package/components/QSafeArea.vue +39 -0
- package/components/QScrollArea.vue +118 -0
- package/components/QSelect.vue +698 -0
- package/components/QSeparator.vue +39 -0
- package/components/QSidebar.vue +153 -0
- package/components/QSidebarContent.vue +14 -0
- package/components/QSidebarFooter.vue +14 -0
- package/components/QSidebarHeader.vue +14 -0
- package/components/QSidebarMenu.vue +9 -0
- package/components/QSidebarMenuButton.vue +59 -0
- package/components/QSidebarMenuItem.vue +9 -0
- package/components/QSidebarTrigger.vue +32 -0
- package/components/QSkeleton.vue +95 -0
- package/components/QSlider.vue +205 -0
- package/components/QSpace.vue +10 -0
- package/components/QSpinner.vue +172 -0
- package/components/QSplitter.vue +145 -0
- package/components/QSticky.vue +181 -0
- package/components/QSwipeCell.vue +217 -0
- package/components/QSwiper.vue +102 -0
- package/components/QSwiperSlide.vue +19 -0
- package/components/QSyntax.vue +171 -0
- package/components/QTab.vue +175 -0
- package/components/QTabPanel.vue +50 -0
- package/components/QTabPanels.vue +106 -0
- package/components/QTable.vue +575 -0
- package/components/QTabs.vue +188 -0
- package/components/QText.vue +316 -0
- package/components/QTextCaption.vue +122 -0
- package/components/QToolbar.vue +28 -0
- package/components/QTooltip.vue +313 -0
- package/components/QUploader.vue +473 -0
- package/components/QVideo.vue +289 -0
- package/components/QVirtualScroll.vue +190 -0
- package/components/internal/QDateCalendar.vue +153 -0
- package/index.ts +151 -0
- package/lib/breakpoints.ts +44 -0
- package/lib/colors.ts +24 -0
- package/lib/config.ts +34 -0
- package/lib/countries.ts +261 -0
- package/lib/formula.ts +630 -0
- package/lib/icons.ts +38 -0
- package/lib/loading.ts +75 -0
- package/lib/overlayBack.ts +124 -0
- package/lib/platform.ts +146 -0
- package/lib/primitive.ts +25 -0
- package/lib/q.ts +287 -0
- package/lib/screen.ts +105 -0
- package/lib/search.ts +35 -0
- package/lib/themeVars.ts +22 -0
- package/lib/useComponentProps.ts +54 -0
- package/lib/utils.ts +7 -0
- package/module.ts +89 -0
- package/package.json +41 -0
- package/scripts/generate-exports.ts +56 -0
- package/styles/main.css +7627 -0
- package/tsconfig.json +33 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QCollapse — API Quasar (pattern QExpansionItem) : <q-collapse v-model label="…" icon-left="…" icon-right="…" caption dense>
|
|
3
|
+
// Équivalent du Collapsible shadcn-vue : animation de hauteur via CSS grid (0fr → 1fr).
|
|
4
|
+
import { computed, nextTick, onMounted, ref, watch } from "vue"
|
|
5
|
+
import { Icon } from "@iconify/vue"
|
|
6
|
+
import { icons } from "../lib/icons"
|
|
7
|
+
import { cn } from "../lib/utils"
|
|
8
|
+
|
|
9
|
+
let collapseSeq = 0
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** État ouvert (v-model) ; si absent, état interne initialisé par defaultOpened */
|
|
13
|
+
modelValue?: boolean
|
|
14
|
+
/** Ouvert par défaut (mode non contrôlé) */
|
|
15
|
+
defaultOpened?: boolean
|
|
16
|
+
/** Texte du header */
|
|
17
|
+
label?: string
|
|
18
|
+
/** Sous-titre du header */
|
|
19
|
+
caption?: string
|
|
20
|
+
/** Icône Iconify à gauche du label */
|
|
21
|
+
iconLeft?: string
|
|
22
|
+
/** Icône Iconify à droite du label (avant le chevron) */
|
|
23
|
+
iconRight?: string
|
|
24
|
+
/** Icône Iconify du chevron (défaut : lucide:chevron-down) */
|
|
25
|
+
expandIcon?: string
|
|
26
|
+
headerClass?: string
|
|
27
|
+
/** Hauteur réduite */
|
|
28
|
+
dense?: boolean
|
|
29
|
+
/** Désactivé */
|
|
30
|
+
disable?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
34
|
+
defaultOpened: false,
|
|
35
|
+
dense: false,
|
|
36
|
+
disable: false,
|
|
37
|
+
headerClass: "",
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const emit = defineEmits<{ "update:modelValue": [value: boolean] }>()
|
|
41
|
+
|
|
42
|
+
const uid = `q-collapse-${collapseSeq++}`
|
|
43
|
+
|
|
44
|
+
const internalOpen = ref(props.defaultOpened)
|
|
45
|
+
const open = computed({
|
|
46
|
+
get: () => props.modelValue ?? internalOpen.value,
|
|
47
|
+
set: (v) => {
|
|
48
|
+
internalOpen.value = v
|
|
49
|
+
emit("update:modelValue", v)
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const toggle = () => {
|
|
54
|
+
if (props.disable) return
|
|
55
|
+
open.value = !open.value
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const headerClasses = computed(() =>
|
|
59
|
+
cn(
|
|
60
|
+
"q-collapse__header",
|
|
61
|
+
props.dense && "q-collapse__header--dense",
|
|
62
|
+
props.disable && "q-collapse__header--disabled",
|
|
63
|
+
props.headerClass,
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
// — Contenu repliable : hauteur mesurée (pattern shadcn/reka-ui) —
|
|
68
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
69
|
+
const innerEl = ref<HTMLElement | null>(null)
|
|
70
|
+
const contentHeight = ref(0)
|
|
71
|
+
const measured = ref(false)
|
|
72
|
+
|
|
73
|
+
const measure = () => {
|
|
74
|
+
const inner = innerEl.value
|
|
75
|
+
if (inner) {
|
|
76
|
+
contentHeight.value = inner.scrollHeight
|
|
77
|
+
measured.value = true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const contentStyle = computed<Record<string, string> | undefined>(() => {
|
|
82
|
+
if (!open.value) return { height: "0px" }
|
|
83
|
+
return measured.value ? { height: `${contentHeight.value}px` } : undefined
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
watch(open, async (v) => {
|
|
87
|
+
if (v) {
|
|
88
|
+
await nextTick()
|
|
89
|
+
measure()
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const onTransitionEnd = () => {
|
|
94
|
+
if (open.value && rootEl.value) rootEl.value.style.height = "auto"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
onMounted(measure)
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<template>
|
|
101
|
+
<div class="q-collapse">
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
class="q-collapse__header"
|
|
105
|
+
:class="headerClasses"
|
|
106
|
+
:aria-expanded="open ? 'true' : 'false'"
|
|
107
|
+
:aria-controls="uid"
|
|
108
|
+
:disabled="disable"
|
|
109
|
+
@click="toggle"
|
|
110
|
+
>
|
|
111
|
+
<span class="q-collapse__header-inner">
|
|
112
|
+
<slot name="header">
|
|
113
|
+
<Icon :icon="iconLeft" v-if="iconLeft" class="q-collapse__lead-icon" aria-hidden="true" />
|
|
114
|
+
<span class="q-collapse__titles">
|
|
115
|
+
<span v-if="label" class="q-collapse__label">{{ label }}</span>
|
|
116
|
+
<span v-if="caption" class="q-collapse__caption">{{ caption }}</span>
|
|
117
|
+
</span>
|
|
118
|
+
<Icon :icon="iconRight" v-if="iconRight" class="q-collapse__icon-right" aria-hidden="true" />
|
|
119
|
+
</slot>
|
|
120
|
+
</span>
|
|
121
|
+
<Icon
|
|
122
|
+
:icon="expandIcon || icons.chevronDown"
|
|
123
|
+
class="q-collapse__expand"
|
|
124
|
+
:class="{ 'q-collapse__expand--rotated': open }"
|
|
125
|
+
aria-hidden="true"
|
|
126
|
+
/>
|
|
127
|
+
</button>
|
|
128
|
+
<div
|
|
129
|
+
ref="rootEl"
|
|
130
|
+
:id="uid"
|
|
131
|
+
class="q-collapse__content"
|
|
132
|
+
:class="{ 'q-collapse__content--open': open }"
|
|
133
|
+
:style="contentStyle"
|
|
134
|
+
@transitionend="onTransitionEnd"
|
|
135
|
+
>
|
|
136
|
+
<div ref="innerEl" class="q-collapse__inner">
|
|
137
|
+
<slot />
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
</template>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QConfigProvider — API Quasar : <q-config-provider :theme="{ colors: { primary: '#ff0000' }, componentProps: { QBtn: { radius: 'md' } } }">
|
|
3
|
+
// Fournit les couleurs du thème à tout le sous-arbre via des variables CSS locales
|
|
4
|
+
// (foreground calculé automatiquement), des props par défaut par composant,
|
|
5
|
+
// et rend automatiquement la pile de dialogues programmatiques ($q.dialog)
|
|
6
|
+
// via QDialogProvider — rien à monter de plus.
|
|
7
|
+
import { computed, inject, onBeforeUnmount, onMounted, provide, ref, watch } from "vue"
|
|
8
|
+
import type { StyleValue } from "vue"
|
|
9
|
+
import QDialogProvider from "./QDialogProvider.vue"
|
|
10
|
+
import QNotifyProvider from "./QNotifyProvider.vue"
|
|
11
|
+
import QLoadingProvider from "./QLoadingProvider.vue"
|
|
12
|
+
import QBottomSheetProvider from "./QBottomSheetProvider.vue"
|
|
13
|
+
import QImagePreviewProvider from "./QImagePreviewProvider.vue"
|
|
14
|
+
import { qConfigKey, qProvidersKey } from "../lib/config"
|
|
15
|
+
import type { QConfigContext, QTheme, ThemeMode } from "../lib/config"
|
|
16
|
+
import { isRadiusScale, RADIUS_VALUES } from "../lib/useComponentProps"
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
/**
|
|
20
|
+
* Thème : objet { mode, colors, componentProps } ou raccourci
|
|
21
|
+
* "light" | "dark" | "system" (mode seul).
|
|
22
|
+
* Ex. : <q-config-provider theme="dark" /> ou
|
|
23
|
+
* :theme="{ mode: 'dark', colors: { primary: '#ff0000' }, componentProps: { QBtn: { radius: 'md' } } }"
|
|
24
|
+
*/
|
|
25
|
+
theme?: QTheme | ThemeMode
|
|
26
|
+
/** Rend un div conteneur ; sinon fournit le thème sans élément DOM */
|
|
27
|
+
render?: boolean
|
|
28
|
+
/** Classe(s) additionnelle(s) sur le conteneur */
|
|
29
|
+
class?: string
|
|
30
|
+
/** Styles CSS additionnels fusionnés au style du conteneur */
|
|
31
|
+
style?: StyleValue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
35
|
+
theme: () => ({}),
|
|
36
|
+
render: true,
|
|
37
|
+
class: "",
|
|
38
|
+
style: undefined,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Imbrication : le thème du parent est fusionné (le plus proche gagne)
|
|
42
|
+
const parent = inject(qConfigKey, null)
|
|
43
|
+
|
|
44
|
+
/** Normalise la prop theme : string (mode) ou objet */
|
|
45
|
+
const normalizeTheme = (t: QTheme | ThemeMode | undefined): QTheme =>
|
|
46
|
+
typeof t === "string" ? { mode: t } : (t ?? {})
|
|
47
|
+
|
|
48
|
+
/** Fusionne deux maps de componentProps : par composant, le plus proche gagne par prop. */
|
|
49
|
+
const mergeComponentProps = (
|
|
50
|
+
a?: Record<string, Record<string, unknown>>,
|
|
51
|
+
b?: Record<string, Record<string, unknown>>,
|
|
52
|
+
): Record<string, Record<string, unknown>> => {
|
|
53
|
+
const base = a ?? {}
|
|
54
|
+
const self = b ?? {}
|
|
55
|
+
const keys = new Set([...Object.keys(base), ...Object.keys(self)])
|
|
56
|
+
const out: Record<string, Record<string, unknown>> = {}
|
|
57
|
+
for (const key of keys) {
|
|
58
|
+
out[key] = { ...base[key], ...self[key] }
|
|
59
|
+
}
|
|
60
|
+
return out
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const mergedTheme = computed<QTheme>(() => {
|
|
64
|
+
const self = normalizeTheme(props.theme)
|
|
65
|
+
const parentTheme = parent?.theme.value
|
|
66
|
+
return {
|
|
67
|
+
mode: self.mode ?? parentTheme?.mode ?? "system",
|
|
68
|
+
colors: { ...parentTheme?.colors, ...self.colors },
|
|
69
|
+
componentProps: mergeComponentProps(parentTheme?.componentProps, self.componentProps),
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// — Mode clair/sombre —
|
|
74
|
+
const systemDark = ref(false)
|
|
75
|
+
let mql: MediaQueryList | null = null
|
|
76
|
+
const updateSystem = () => {
|
|
77
|
+
systemDark.value = mql?.matches ?? false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
onMounted(() => {
|
|
81
|
+
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
|
|
82
|
+
mql = window.matchMedia("(prefers-color-scheme: dark)")
|
|
83
|
+
updateSystem()
|
|
84
|
+
mql.addEventListener("change", updateSystem)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
onBeforeUnmount(() => {
|
|
88
|
+
mql?.removeEventListener("change", updateSystem)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
/** Mode effectif : light | dark | system résolu */
|
|
92
|
+
const isDark = computed<boolean>(() => {
|
|
93
|
+
const mode = mergedTheme.value.mode
|
|
94
|
+
if (mode === "dark") return true
|
|
95
|
+
if (mode === "light") return false
|
|
96
|
+
return systemDark.value
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Applique .dark sur le conteneur ET sur <html> (les overlays téléportés au body
|
|
100
|
+
// — dialog, sheets — doivent aussi passer en sombre)
|
|
101
|
+
watch(
|
|
102
|
+
isDark,
|
|
103
|
+
(dark) => {
|
|
104
|
+
if (typeof document !== "undefined") {
|
|
105
|
+
document.documentElement.classList.toggle("dark", dark)
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{ immediate: true },
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
provide<QConfigContext>(qConfigKey, { theme: mergedTheme, isDark })
|
|
112
|
+
|
|
113
|
+
// Providers intégrés ($q.dialog + $q.notify) : rendus UNE fois par le
|
|
114
|
+
// QConfigProvider le plus externe (les imbriqués ne re-rendent pas → pas de doublons)
|
|
115
|
+
const hasProviders = inject(qProvidersKey, false)
|
|
116
|
+
provide(qProvidersKey, true)
|
|
117
|
+
const isProvidersRoot = computed(() => !hasProviders)
|
|
118
|
+
|
|
119
|
+
/** Calcule un foreground lisible (blanc ou sombre) pour un fond donné. */
|
|
120
|
+
const textColorFor = (bg: string): string => {
|
|
121
|
+
let hex = bg.trim().replace(/^#/, "")
|
|
122
|
+
if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("")
|
|
123
|
+
if (!/^[0-9a-f]{6}$/i.test(hex)) return "#ffffff"
|
|
124
|
+
const r = parseInt(hex.slice(0, 2), 16)
|
|
125
|
+
const g = parseInt(hex.slice(2, 4), 16)
|
|
126
|
+
const b = parseInt(hex.slice(4, 6), 16)
|
|
127
|
+
const luminance = 0.299 * r + 0.587 * g + 0.114 * b
|
|
128
|
+
return luminance > 150 ? "#1d1d1d" : "#ffffff"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const themeStyle = computed<Record<string, string>>(() => {
|
|
132
|
+
const style: Record<string, string> = {
|
|
133
|
+
"color-scheme": isDark.value ? "dark" : "light",
|
|
134
|
+
}
|
|
135
|
+
for (const [token, value] of Object.entries(mergedTheme.value.colors ?? {})) {
|
|
136
|
+
if (!value) continue
|
|
137
|
+
style[`--${token}`] = value
|
|
138
|
+
style[`--${token}-foreground`] = textColorFor(value)
|
|
139
|
+
}
|
|
140
|
+
// Arrondi global : composantProps.default.radius → --q-radius hérité par TOUS
|
|
141
|
+
// les composants dont le CSS utilise var(--q-radius). Une prop/override
|
|
142
|
+
// spécifique (useRadius) pose son propre --q-radius inline et prime.
|
|
143
|
+
const global = mergedTheme.value.componentProps?.default?.radius
|
|
144
|
+
if (isRadiusScale(global)) style["--q-radius"] = RADIUS_VALUES[global]
|
|
145
|
+
return style
|
|
146
|
+
})
|
|
147
|
+
</script>
|
|
148
|
+
|
|
149
|
+
<template>
|
|
150
|
+
<q-dialog-provider v-if="isProvidersRoot">
|
|
151
|
+
<q-bottom-sheet-provider />
|
|
152
|
+
<q-notify-provider />
|
|
153
|
+
<q-loading-provider />
|
|
154
|
+
<q-image-preview-provider />
|
|
155
|
+
<div v-if="render" class="q-config-provider" :class="[props.class, { dark: isDark }]" :style="[themeStyle, props.style]">
|
|
156
|
+
<slot />
|
|
157
|
+
</div>
|
|
158
|
+
<slot v-else />
|
|
159
|
+
</q-dialog-provider>
|
|
160
|
+
<div v-else-if="render" class="q-config-provider" :class="[props.class, { dark: isDark }]" :style="[themeStyle, props.style]">
|
|
161
|
+
<slot />
|
|
162
|
+
</div>
|
|
163
|
+
<slot v-else />
|
|
164
|
+
</template>
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QContainer — conteneur centré à largeur max : <q-container max-width="1200px" fluid>
|
|
3
|
+
// background-effect : "grid" (grille de carrés) | "aurora" (halos colorés animés).
|
|
4
|
+
import { computed, onBeforeUnmount, onMounted, ref } from "vue"
|
|
5
|
+
import { cn } from "../lib/utils"
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Largeur max du contenu (défaut 1200px) */
|
|
9
|
+
maxWidth?: string | number
|
|
10
|
+
/** Supprime la largeur max (pleine largeur, padding conservé) */
|
|
11
|
+
fluid?: boolean
|
|
12
|
+
/** Padding horizontal (défaut 16px) */
|
|
13
|
+
padding?: string
|
|
14
|
+
/** Effet d'arrière-plan : "grid" | "aurora" | "star" | "falling-stars" | "flickering-grid" | "interactive-grid" | "wave" (vagues qui glissent) */
|
|
15
|
+
backgroundEffect?:
|
|
16
|
+
| "grid"
|
|
17
|
+
| "aurora"
|
|
18
|
+
| "star"
|
|
19
|
+
| "falling-stars"
|
|
20
|
+
| "flickering-grid"
|
|
21
|
+
| "interactive-grid"
|
|
22
|
+
| "wave"
|
|
23
|
+
/** Couleur des lignes de la grille (background-effect="grid") — tout format CSS */
|
|
24
|
+
gridColor?: string
|
|
25
|
+
/** Espacement entre les lignes de la grille (défaut 32px) */
|
|
26
|
+
gridSpacing?: string
|
|
27
|
+
/** Couleur des étoiles (background-effect="star") — tout format CSS */
|
|
28
|
+
starColor?: string
|
|
29
|
+
/** Espacement entre les étoiles (défaut 24px) */
|
|
30
|
+
starSpacing?: string
|
|
31
|
+
/** Taille des points d'étoile (défaut 1px) */
|
|
32
|
+
starDotSize?: string
|
|
33
|
+
/** Couleur du premier halo (background-effect="aurora", défaut cyan) */
|
|
34
|
+
auroraColor?: string
|
|
35
|
+
/** Couleur du second halo (background-effect="aurora", défaut violet) */
|
|
36
|
+
auroraColor2?: string
|
|
37
|
+
/** Couleur de la première vague (background-effect="wave", défaut bleu) */
|
|
38
|
+
waveColor?: string
|
|
39
|
+
/** Couleur de la seconde vague (background-effect="wave", défaut violet) */
|
|
40
|
+
waveColor2?: string
|
|
41
|
+
class?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
45
|
+
maxWidth: "1200px",
|
|
46
|
+
fluid: false,
|
|
47
|
+
padding: "16px",
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const containerStyle = computed<Record<string, string>>(() => {
|
|
51
|
+
const style: Record<string, string> = {
|
|
52
|
+
"--q-container-max": typeof props.maxWidth === "number" ? `${props.maxWidth}px` : props.maxWidth,
|
|
53
|
+
"--q-container-pad": props.padding,
|
|
54
|
+
}
|
|
55
|
+
if (props.gridColor) style["--q-grid-color"] = props.gridColor
|
|
56
|
+
if (props.gridSpacing) style["--q-grid-spacing"] = props.gridSpacing
|
|
57
|
+
if (props.starColor) style["--q-star-color"] = props.starColor
|
|
58
|
+
if (props.starSpacing) style["--q-star-spacing"] = props.starSpacing
|
|
59
|
+
if (props.starDotSize) style["--q-star-dot-size"] = props.starDotSize
|
|
60
|
+
if (props.auroraColor) style["--q-aurora-color"] = props.auroraColor
|
|
61
|
+
if (props.auroraColor2) style["--q-aurora-color-2"] = props.auroraColor2
|
|
62
|
+
if (props.waveColor) style["--q-wave-color"] = props.waveColor
|
|
63
|
+
if (props.waveColor2) style["--q-wave-color-2"] = props.waveColor2
|
|
64
|
+
return style
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const containerClasses = computed(() =>
|
|
68
|
+
cn(
|
|
69
|
+
"q-container",
|
|
70
|
+
props.fluid && "q-container--fluid",
|
|
71
|
+
props.backgroundEffect && "q-container--bg",
|
|
72
|
+
props.class,
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// — Étoiles filantes : positions + minutage (durées/délais en s) —
|
|
77
|
+
interface FallingStar {
|
|
78
|
+
left: string
|
|
79
|
+
delay: number
|
|
80
|
+
duration: number
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const FALLING_STARS: FallingStar[] = [
|
|
84
|
+
{ left: "8%", delay: 0, duration: 9 },
|
|
85
|
+
{ left: "22%", delay: 2.5, duration: 11 },
|
|
86
|
+
{ left: "35%", delay: 5, duration: 8.5 },
|
|
87
|
+
{ left: "48%", delay: 1.5, duration: 10 },
|
|
88
|
+
{ left: "58%", delay: 4, duration: 9.5 },
|
|
89
|
+
{ left: "70%", delay: 6.5, duration: 8 },
|
|
90
|
+
{ left: "82%", delay: 3, duration: 10.5 },
|
|
91
|
+
{ left: "92%", delay: 7, duration: 9 },
|
|
92
|
+
{ left: "15%", delay: 8, duration: 11.5 },
|
|
93
|
+
{ left: "63%", delay: 9, duration: 8.5 },
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
const starStyle = (s: FallingStar): Record<string, string> => ({
|
|
97
|
+
left: s.left,
|
|
98
|
+
"--q-star-delay": `${s.delay}s`,
|
|
99
|
+
"--q-star-duration": `${s.duration}s`,
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// — Grille qui grésille : cellules qui clignotent à des positions variées —
|
|
103
|
+
interface FlickerCell {
|
|
104
|
+
left: string
|
|
105
|
+
top: string
|
|
106
|
+
delay: number
|
|
107
|
+
duration: number
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const FLICKER_CELLS: FlickerCell[] = [
|
|
111
|
+
{ left: "6%", top: "18%", delay: 0, duration: 2.4 },
|
|
112
|
+
{ left: "14%", top: "62%", delay: 0.8, duration: 1.9 },
|
|
113
|
+
{ left: "23%", top: "34%", delay: 1.6, duration: 2.8 },
|
|
114
|
+
{ left: "31%", top: "78%", delay: 0.3, duration: 2.1 },
|
|
115
|
+
{ left: "42%", top: "12%", delay: 2.2, duration: 1.7 },
|
|
116
|
+
{ left: "50%", top: "48%", delay: 1.1, duration: 2.6 },
|
|
117
|
+
{ left: "58%", top: "84%", delay: 0.5, duration: 2 },
|
|
118
|
+
{ left: "66%", top: "28%", delay: 2.7, duration: 1.8 },
|
|
119
|
+
{ left: "75%", top: "58%", delay: 1.4, duration: 2.3 },
|
|
120
|
+
{ left: "83%", top: "20%", delay: 0.9, duration: 2.5 },
|
|
121
|
+
{ left: "90%", top: "70%", delay: 2, duration: 1.9 },
|
|
122
|
+
{ left: "36%", top: "92%", delay: 1.8, duration: 2.2 },
|
|
123
|
+
]
|
|
124
|
+
|
|
125
|
+
const flickerStyle = (c: FlickerCell): Record<string, string> => ({
|
|
126
|
+
left: c.left,
|
|
127
|
+
top: c.top,
|
|
128
|
+
"--q-flicker-delay": `${c.delay}s`,
|
|
129
|
+
"--q-flicker-duration": `${c.duration}s`,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// — Grille interactive : le glow suit la souris (vars --q-mouse-x/y sur le root) —
|
|
133
|
+
const rootEl = ref<HTMLElement | null>(null)
|
|
134
|
+
|
|
135
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
136
|
+
if (props.backgroundEffect !== "interactive-grid" || !rootEl.value) return
|
|
137
|
+
const rect = rootEl.value.getBoundingClientRect()
|
|
138
|
+
rootEl.value.style.setProperty("--q-mouse-x", `${e.clientX - rect.left}px`)
|
|
139
|
+
rootEl.value.style.setProperty("--q-mouse-y", `${e.clientY - rect.top}px`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
onMounted(() => rootEl.value?.addEventListener("mousemove", onMouseMove))
|
|
143
|
+
onBeforeUnmount(() => rootEl.value?.removeEventListener("mousemove", onMouseMove))
|
|
144
|
+
</script>
|
|
145
|
+
|
|
146
|
+
<template>
|
|
147
|
+
<div ref="rootEl" class="q-container" :class="containerClasses" :style="containerStyle">
|
|
148
|
+
<div
|
|
149
|
+
v-if="backgroundEffect"
|
|
150
|
+
class="q-container__bg"
|
|
151
|
+
:class="`q-container__bg--${backgroundEffect}`"
|
|
152
|
+
aria-hidden="true"
|
|
153
|
+
>
|
|
154
|
+
<template v-if="backgroundEffect === 'falling-stars'">
|
|
155
|
+
<span
|
|
156
|
+
v-for="(s, i) in FALLING_STARS"
|
|
157
|
+
:key="i"
|
|
158
|
+
class="q-container__star"
|
|
159
|
+
:style="starStyle(s)"
|
|
160
|
+
/>
|
|
161
|
+
</template>
|
|
162
|
+
<template v-else-if="backgroundEffect === 'flickering-grid'">
|
|
163
|
+
<span
|
|
164
|
+
v-for="(c, i) in FLICKER_CELLS"
|
|
165
|
+
:key="i"
|
|
166
|
+
class="q-container__flicker"
|
|
167
|
+
:style="flickerStyle(c)"
|
|
168
|
+
/>
|
|
169
|
+
</template>
|
|
170
|
+
<span
|
|
171
|
+
v-else-if="backgroundEffect === 'interactive-grid'"
|
|
172
|
+
class="q-container__glow"
|
|
173
|
+
/>
|
|
174
|
+
<template v-else-if="backgroundEffect === 'wave'">
|
|
175
|
+
<svg
|
|
176
|
+
class="q-container__wave q-container__wave--1"
|
|
177
|
+
viewBox="0 0 960 128"
|
|
178
|
+
preserveAspectRatio="none"
|
|
179
|
+
aria-hidden="true"
|
|
180
|
+
>
|
|
181
|
+
<path
|
|
182
|
+
d="M0,64 C80,100 160,28 240,64 C320,100 400,28 480,64 C560,100 640,28 720,64 C800,100 880,28 960,64"
|
|
183
|
+
fill="var(--q-wave-color)"
|
|
184
|
+
/>
|
|
185
|
+
</svg>
|
|
186
|
+
<svg
|
|
187
|
+
class="q-container__wave q-container__wave--2"
|
|
188
|
+
viewBox="0 0 960 128"
|
|
189
|
+
preserveAspectRatio="none"
|
|
190
|
+
aria-hidden="true"
|
|
191
|
+
>
|
|
192
|
+
<path
|
|
193
|
+
d="M0,64 C80,100 160,28 240,64 C320,100 400,28 480,64 C560,100 640,28 720,64 C800,100 880,28 960,64"
|
|
194
|
+
fill="var(--q-wave-color-2)"
|
|
195
|
+
/>
|
|
196
|
+
</svg>
|
|
197
|
+
</template>
|
|
198
|
+
</div>
|
|
199
|
+
<slot />
|
|
200
|
+
</div>
|
|
201
|
+
</template>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// QCountDown — compte à rebours : <q-count-down :time="90" format="mm:ss" @end="…" />
|
|
3
|
+
// ou vers une date cible : <q-count-down :to="new Date('2026-12-31')" />
|
|
4
|
+
// Format : DD | HH | mm | ss | SSS (défaut "HH:mm:ss"). Slot : données brutes + progress.
|
|
5
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Durée initiale en SECONDES (ignoré si `to` est fourni) */
|
|
9
|
+
time?: number
|
|
10
|
+
/** Date cible : Date | timestamp | chaîne ISO — compte jusqu'à cette date */
|
|
11
|
+
to?: string | number | Date
|
|
12
|
+
/** Démarre automatiquement (défaut true) */
|
|
13
|
+
autoStart?: boolean
|
|
14
|
+
/** Pause / reprise (contrôle externe) */
|
|
15
|
+
pause?: boolean
|
|
16
|
+
/** Format d'affichage : DD, HH, mm, ss, SSS (défaut "HH:mm:ss") */
|
|
17
|
+
format?: string
|
|
18
|
+
/** Élément rendu (défaut span) */
|
|
19
|
+
tag?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
23
|
+
time: 0,
|
|
24
|
+
autoStart: true,
|
|
25
|
+
pause: false,
|
|
26
|
+
format: "HH:mm:ss",
|
|
27
|
+
tag: "span",
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const emit = defineEmits<{ end: [] }>()
|
|
31
|
+
|
|
32
|
+
const totalMs = ref(0)
|
|
33
|
+
const remainingMs = ref(0)
|
|
34
|
+
const endAt = ref(0)
|
|
35
|
+
const running = ref(false)
|
|
36
|
+
let timer: ReturnType<typeof setInterval> | null = null
|
|
37
|
+
|
|
38
|
+
const computeTotal = () => {
|
|
39
|
+
if (props.to !== undefined) {
|
|
40
|
+
const t =
|
|
41
|
+
props.to instanceof Date
|
|
42
|
+
? props.to.getTime()
|
|
43
|
+
: typeof props.to === "number"
|
|
44
|
+
? props.to
|
|
45
|
+
: new Date(props.to).getTime()
|
|
46
|
+
totalMs.value = Math.max(0, t - Date.now())
|
|
47
|
+
} else {
|
|
48
|
+
totalMs.value = Math.max(0, props.time * 1000)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const stopTimer = () => {
|
|
53
|
+
if (timer) clearInterval(timer)
|
|
54
|
+
timer = null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const tick = () => {
|
|
58
|
+
const rem = Math.max(0, endAt.value - Date.now())
|
|
59
|
+
remainingMs.value = rem
|
|
60
|
+
if (rem <= 0) {
|
|
61
|
+
running.value = false
|
|
62
|
+
stopTimer()
|
|
63
|
+
emit("end")
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Démarre (ou reprend) le compte */
|
|
68
|
+
const start = () => {
|
|
69
|
+
if (props.pause || remainingMs.value <= 0 || running.value) return
|
|
70
|
+
endAt.value = Date.now() + remainingMs.value
|
|
71
|
+
stopTimer()
|
|
72
|
+
timer = setInterval(tick, 250)
|
|
73
|
+
running.value = true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Met en pause */
|
|
77
|
+
const pauseTimer = () => {
|
|
78
|
+
running.value = false
|
|
79
|
+
stopTimer()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Réinitialise à la durée initiale (et relance si autoStart) */
|
|
83
|
+
const reset = () => {
|
|
84
|
+
computeTotal()
|
|
85
|
+
remainingMs.value = totalMs.value
|
|
86
|
+
if (props.autoStart && !props.pause) start()
|
|
87
|
+
else running.value = false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Temps restant (ms) */
|
|
91
|
+
const getRemaining = () => remainingMs.value
|
|
92
|
+
/** Progression écoulée 0 → 1 */
|
|
93
|
+
const getProgress = () => (totalMs.value > 0 ? 1 - remainingMs.value / totalMs.value : 0)
|
|
94
|
+
|
|
95
|
+
defineExpose({ start, pause: pauseTimer, reset, getRemaining, getProgress })
|
|
96
|
+
|
|
97
|
+
onMounted(() => {
|
|
98
|
+
computeTotal()
|
|
99
|
+
remainingMs.value = totalMs.value
|
|
100
|
+
if (props.autoStart && !props.pause) start()
|
|
101
|
+
})
|
|
102
|
+
onBeforeUnmount(stopTimer)
|
|
103
|
+
|
|
104
|
+
watch(
|
|
105
|
+
() => props.pause,
|
|
106
|
+
(v) => {
|
|
107
|
+
if (v) pauseTimer()
|
|
108
|
+
else if (remainingMs.value > 0) start()
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
watch(
|
|
112
|
+
() => [props.time, props.to],
|
|
113
|
+
() => reset(),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// — Unitées + format —
|
|
117
|
+
const units = computed(() => {
|
|
118
|
+
const rem = remainingMs.value
|
|
119
|
+
const days = Math.floor(rem / 86400000)
|
|
120
|
+
const hours = Math.floor((rem % 86400000) / 3600000)
|
|
121
|
+
const minutes = Math.floor((rem % 3600000) / 60000)
|
|
122
|
+
const seconds = Math.floor((rem % 60000) / 1000)
|
|
123
|
+
const ms = rem % 1000
|
|
124
|
+
return { days, hours, minutes, seconds, ms, remaining: rem }
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const formatted = computed(() => {
|
|
128
|
+
const { days, hours, minutes, seconds, ms } = units.value
|
|
129
|
+
const totalHours = Math.floor(remainingMs.value / 3600000)
|
|
130
|
+
const h = props.format.includes("D") ? hours : totalHours
|
|
131
|
+
const pad = (n: number, len = 2) => String(n).padStart(len, "0")
|
|
132
|
+
return props.format
|
|
133
|
+
.replace(/SSS/g, pad(ms, 3))
|
|
134
|
+
.replace(/SS/g, pad(Math.floor(ms / 10), 2))
|
|
135
|
+
.replace(/S/g, String(Math.floor(ms / 100)))
|
|
136
|
+
.replace(/DD/g, pad(days))
|
|
137
|
+
.replace(/D/g, String(days))
|
|
138
|
+
.replace(/HH/g, pad(h))
|
|
139
|
+
.replace(/H/g, String(h))
|
|
140
|
+
.replace(/mm/g, pad(minutes))
|
|
141
|
+
.replace(/m/g, String(minutes))
|
|
142
|
+
.replace(/ss/g, pad(seconds))
|
|
143
|
+
.replace(/s/g, String(seconds))
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const progress = computed(() => (totalMs.value > 0 ? 1 - remainingMs.value / totalMs.value : 0))
|
|
147
|
+
</script>
|
|
148
|
+
|
|
149
|
+
<template>
|
|
150
|
+
<component
|
|
151
|
+
:is="tag"
|
|
152
|
+
class="q-count-down"
|
|
153
|
+
:class="{ 'q-count-down--done': remainingMs <= 0 && totalMs > 0 }"
|
|
154
|
+
>
|
|
155
|
+
<slot
|
|
156
|
+
v-bind="{ ...units, progress, running, formatted }"
|
|
157
|
+
:formatted="formatted"
|
|
158
|
+
:progress="progress"
|
|
159
|
+
:running="running"
|
|
160
|
+
>
|
|
161
|
+
{{ formatted }}
|
|
162
|
+
</slot>
|
|
163
|
+
</component>
|
|
164
|
+
</template>
|