@dnax/ui 0.0.44 → 0.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/QApp.vue +13 -6
- package/components/QFooter.vue +16 -2
- package/components/QHeader.vue +16 -2
- package/components/QLayout.vue +138 -0
- package/components/QSelect.vue +91 -56
- package/components/QSidebar.vue +28 -3
- package/components/QTiptap.vue +1 -0
- package/index.ts +3 -1
- package/lib/layout.test.ts +125 -0
- package/lib/layout.ts +181 -0
- package/lib/useComponentProps.test.ts +36 -0
- package/lib/useComponentProps.ts +7 -1
- package/module.ts +0 -18
- package/package.json +1 -2
- package/scripts/generate-exports.ts +9 -0
- package/styles/main.css +112 -20
- package/components/QVideo.vue +0 -305
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Tests unitaires de l'analyse du prop `view` de QLayout (matrice 3×3).
|
|
2
|
+
import { describe, expect, it } from "bun:test"
|
|
3
|
+
import { DEFAULT_LAYOUT_VIEW, parseView } from "./layout"
|
|
4
|
+
|
|
5
|
+
describe("parseView", () => {
|
|
6
|
+
it("applique la vue par défaut sans argument", () => {
|
|
7
|
+
const v = parseView()
|
|
8
|
+
|
|
9
|
+
expect(v.view).toBe(DEFAULT_LAYOUT_VIEW)
|
|
10
|
+
expect(v.view).toBe("hHh LpR fFf")
|
|
11
|
+
expect(v.error).toBeUndefined()
|
|
12
|
+
// Header et footer pleine largeur, drawers dans la rangée du milieu
|
|
13
|
+
expect(v.gridTemplateAreas).toBe('"h h h" "l p r" "f f f"')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it("déduit l'aire de chaque zone", () => {
|
|
17
|
+
const v = parseView("hHh LpR fFf")
|
|
18
|
+
|
|
19
|
+
expect(v.zones.header.area).toBe("h")
|
|
20
|
+
expect(v.zones.footer.area).toBe("f")
|
|
21
|
+
expect(v.zones.left.area).toBe("l")
|
|
22
|
+
expect(v.zones.right.area).toBe("r")
|
|
23
|
+
expect(v.zones.page.area).toBe("p")
|
|
24
|
+
expect(v.zones.page.cells).toEqual([[1, 1]])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("lit la casse comme « reste en place » (majuscule) ou « défile » (minuscule)", () => {
|
|
28
|
+
const fixed = parseView("hHh LpR fFf")
|
|
29
|
+
expect(fixed.zones.header.fixed).toBe(true)
|
|
30
|
+
expect(fixed.zones.footer.fixed).toBe(true)
|
|
31
|
+
expect(fixed.zones.left.fixed).toBe(true)
|
|
32
|
+
expect(fixed.zones.right.fixed).toBe(true)
|
|
33
|
+
|
|
34
|
+
const flowing = parseView("hhh lpR fff")
|
|
35
|
+
expect(flowing.zones.header.fixed).toBe(false)
|
|
36
|
+
expect(flowing.zones.footer.fixed).toBe(false)
|
|
37
|
+
expect(flowing.zones.left.fixed).toBe(false)
|
|
38
|
+
expect(flowing.zones.right.fixed).toBe(true) // une seule majuscule suffit
|
|
39
|
+
expect(flowing.gridTemplateAreas).toBe('"h h h" "l p r" "f f f"')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("fait démarrer le drawer gauche au-dessus du header (lhh)", () => {
|
|
43
|
+
const v = parseView("lhh LpR fff")
|
|
44
|
+
|
|
45
|
+
expect(v.gridTemplateAreas).toBe('"l h h" "l p r" "f f f"')
|
|
46
|
+
expect(v.zones.left.cells).toEqual([
|
|
47
|
+
[0, 0],
|
|
48
|
+
[1, 0],
|
|
49
|
+
])
|
|
50
|
+
expect(v.zones.header.cells).toEqual([
|
|
51
|
+
[0, 1],
|
|
52
|
+
[0, 2],
|
|
53
|
+
])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("fait descendre le drawer droit jusqu'au footer (ffR)", () => {
|
|
57
|
+
const v = parseView("hhr lpR ffR")
|
|
58
|
+
|
|
59
|
+
expect(v.gridTemplateAreas).toBe('"h h r" "l p r" "f f r"')
|
|
60
|
+
expect(v.zones.right.cells).toEqual([
|
|
61
|
+
[0, 2],
|
|
62
|
+
[1, 2],
|
|
63
|
+
[2, 2],
|
|
64
|
+
])
|
|
65
|
+
expect(v.zones.right.fixed).toBe(true)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("accepte les cellules vides (.)", () => {
|
|
69
|
+
const v = parseView("hHh .p. fFf")
|
|
70
|
+
|
|
71
|
+
expect(v.gridTemplateAreas).toBe('"h h h" ". p ." "f f f"')
|
|
72
|
+
expect(v.zones.left.cells).toEqual([])
|
|
73
|
+
expect(v.zones.left.area).toBeUndefined()
|
|
74
|
+
expect(v.zones.right.cells).toEqual([])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("marque la page « en place » si son p est majuscule", () => {
|
|
78
|
+
expect(parseView("hhh LPR fff").zones.page.fixed).toBe(true)
|
|
79
|
+
expect(parseView("hhh LpR fff").zones.page.fixed).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("rejette une chaîne de mauvaise longueur", () => {
|
|
83
|
+
const v = parseView("hHh lpR")
|
|
84
|
+
|
|
85
|
+
expect(v.view).toBe(DEFAULT_LAYOUT_VIEW)
|
|
86
|
+
expect(v.error).toContain("3 groupes de 3 caractères")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("rejette une lettre inconnue", () => {
|
|
90
|
+
const v = parseView("hHh xpR fFf")
|
|
91
|
+
|
|
92
|
+
expect(v.view).toBe(DEFAULT_LAYOUT_VIEW)
|
|
93
|
+
expect(v.error).toContain('lettre "x" inconnue')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("rejette une zone hors de sa rangée", () => {
|
|
97
|
+
const v = parseView("fhf LpR fFf")
|
|
98
|
+
|
|
99
|
+
expect(v.view).toBe(DEFAULT_LAYOUT_VIEW)
|
|
100
|
+
expect(v.error).toContain("rangée 1")
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("exige une page dans la rangée du milieu", () => {
|
|
104
|
+
const v = parseView("hHh lrr fFf")
|
|
105
|
+
|
|
106
|
+
expect(v.view).toBe(DEFAULT_LAYOUT_VIEW)
|
|
107
|
+
expect(v.error).toContain('au moins un "p"')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("rejette une zone non rectangulaire", () => {
|
|
111
|
+
// `l` en (0,0) et (2,0) sans (1,0) : grid-template-areas serait invalide
|
|
112
|
+
const v = parseView("lhr plr lfr")
|
|
113
|
+
|
|
114
|
+
expect(v.view).toBe(DEFAULT_LAYOUT_VIEW)
|
|
115
|
+
expect(v.error).toContain('zone "left"')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it("retombe toujours sur une vue utilisable", () => {
|
|
119
|
+
const v = parseView("n'importe quoi")
|
|
120
|
+
|
|
121
|
+
expect(v.error).toBeDefined()
|
|
122
|
+
expect(v.zones.page.cells).toEqual([[1, 1]])
|
|
123
|
+
expect(v.gridTemplateAreas).toBe('"h h h" "l p r" "f f f"')
|
|
124
|
+
})
|
|
125
|
+
})
|
package/lib/layout.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Analyse du prop `view` de QLayout — matrice 3×3 de la fenêtre (modèle Quasar).
|
|
2
|
+
//
|
|
3
|
+
// `view` = 11 caractères : 3 (rangée header) + espace + 3 (rangée milieu) + espace +
|
|
4
|
+
// 3 (rangée footer). Chaque caractère dit quelle zone occupe cette cellule :
|
|
5
|
+
// l = drawer gauche, h = header, r = drawer droit, f = footer, p = page (centre),
|
|
6
|
+
// . = cellule vide.
|
|
7
|
+
// La casse compte : **majuscule = la zone reste en place** au scroll (Quasar :
|
|
8
|
+
// « position fixe »), **minuscule = elle défile avec la page**. Une zone qui occupe
|
|
9
|
+
// plusieurs cellules les occupe forcément en rectangle (exigence de
|
|
10
|
+
// `grid-template-areas`) ; `hHh lpR fFf` donne donc un header pleine largeur, un
|
|
11
|
+
// drawer gauche sous le header et un drawer droit pleine hauteur.
|
|
12
|
+
//
|
|
13
|
+
// Aucune dépendance au DOM : le SFC pose `grid-template-areas` sur `.q-layout`, et
|
|
14
|
+
// chaque enfant (q-header, q-footer, q-sidebar) lit sa cellule dans le contexte
|
|
15
|
+
// injecté — d'où l'export de `qLayoutKey`.
|
|
16
|
+
//
|
|
17
|
+
// Modèle retenu par dnax.ui (« A ») : **scroll du document + `position: sticky`**,
|
|
18
|
+
// pas le scroll interne de Quasar. Conséquence sur les lettres : la casse se voit
|
|
19
|
+
// sur le header (`H` reste collé / `h` défile) et sur les drawers (`L`/`R` collés et
|
|
20
|
+
// hauts de 100dvh / `l`/`r` dans le flux), alors que `F`/`f` sont **équivalents** (la
|
|
21
|
+
// rangée footer est la dernière : le footer tombe en fin de page dans les deux cas) et
|
|
22
|
+
// que `P` est sans effet (la page EST ce qui défile).
|
|
23
|
+
|
|
24
|
+
import type { InjectionKey, Ref } from "vue"
|
|
25
|
+
|
|
26
|
+
/** Zone de la matrice (une par composant concerné) */
|
|
27
|
+
export type LayoutZone = "left" | "header" | "right" | "page" | "footer"
|
|
28
|
+
|
|
29
|
+
/** Vue par défaut : header et footer pleine largeur, les deux drawers restent en place */
|
|
30
|
+
export const DEFAULT_LAYOUT_VIEW = "hHh LpR fFf"
|
|
31
|
+
|
|
32
|
+
/** Lettres acceptées par rangée — la matrice n'a de sens qu'avec ces zones-là */
|
|
33
|
+
const ROW_LETTERS: readonly (readonly string[])[] = [
|
|
34
|
+
["l", "h", "r"], // rangée header : drawer gauche, header, drawer droit
|
|
35
|
+
["l", "p", "r"], // rangée milieu : drawer gauche, page, drawer droit
|
|
36
|
+
["l", "f", "r"], // rangée footer : drawer gauche, footer, drawer droit
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
/** Lettre → zone */
|
|
40
|
+
const LETTER_ZONE: Record<string, LayoutZone> = {
|
|
41
|
+
l: "left",
|
|
42
|
+
h: "header",
|
|
43
|
+
r: "right",
|
|
44
|
+
p: "page",
|
|
45
|
+
f: "footer",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Cellule vide (aucune zone) */
|
|
49
|
+
const EMPTY_CELL = "."
|
|
50
|
+
|
|
51
|
+
export interface LayoutZonePlacement {
|
|
52
|
+
/** Nom d'aire CSS à passer à `grid-area` — `undefined` si la zone n'occupe aucune cellule */
|
|
53
|
+
area?: string
|
|
54
|
+
/** La zone reste en place au scroll (au moins une de ses lettres en majuscule) */
|
|
55
|
+
fixed: boolean
|
|
56
|
+
/** Cellules occupées, `[ligne, colonne]` en base 0 */
|
|
57
|
+
cells: Array<[number, number]>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface LayoutView {
|
|
61
|
+
/** Chaîne retenue — la vue par défaut quand la chaîne fournie est invalide */
|
|
62
|
+
view: string
|
|
63
|
+
/** `grid-template-areas` prêt à poser sur `.q-layout` */
|
|
64
|
+
gridTemplateAreas: string
|
|
65
|
+
/** Placement de chaque zone */
|
|
66
|
+
zones: Record<LayoutZone, LayoutZonePlacement>
|
|
67
|
+
/** Raison du rejet de la chaîne fournie (`undefined` si elle était valide) */
|
|
68
|
+
error?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Contexte fourni par QLayout à ses enfants (q-header, q-footer, q-sidebar) */
|
|
72
|
+
export interface QLayoutContext {
|
|
73
|
+
/** Placement de chaque zone, déduit de `view` (suit les changements de `view`) */
|
|
74
|
+
zones: Readonly<Ref<Record<LayoutZone, LayoutZonePlacement>>>
|
|
75
|
+
/** QLayout est utilisé comme conteneur (hauteur explicite requise) */
|
|
76
|
+
container: Readonly<Ref<boolean>>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Clé d'injection du contexte QLayout — lue par les composants enfants */
|
|
80
|
+
export const qLayoutKey: InjectionKey<QLayoutContext> = Symbol("q-layout")
|
|
81
|
+
|
|
82
|
+
const emptyZones = (): Record<LayoutZone, LayoutZonePlacement> => ({
|
|
83
|
+
left: { fixed: false, cells: [] },
|
|
84
|
+
header: { fixed: false, cells: [] },
|
|
85
|
+
right: { fixed: false, cells: [] },
|
|
86
|
+
page: { fixed: false, cells: [] },
|
|
87
|
+
footer: { fixed: false, cells: [] },
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
/** Les 3 rangées de 3 lettres d'une vue (chaîne déjà validée) */
|
|
91
|
+
const rowsOf = (view: string) => view.trim().split(/\s+/) as [string, string, string]
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Construit le résultat à partir d'une vue **valide** : cellules de chaque zone,
|
|
95
|
+
* drapeau « reste en place », aires CSS.
|
|
96
|
+
*/
|
|
97
|
+
function build(view: string): LayoutView {
|
|
98
|
+
const rows = rowsOf(view)
|
|
99
|
+
const zones = emptyZones()
|
|
100
|
+
const areas: string[][] = []
|
|
101
|
+
|
|
102
|
+
rows.forEach((row, r) => {
|
|
103
|
+
const cells = [...row]
|
|
104
|
+
areas.push(cells.map((char) => (char === EMPTY_CELL ? EMPTY_CELL : char.toLowerCase())))
|
|
105
|
+
cells.forEach((char, c) => {
|
|
106
|
+
if (char === EMPTY_CELL) return
|
|
107
|
+
const zone = LETTER_ZONE[char.toLowerCase()]!
|
|
108
|
+
zones[zone].cells.push([r, c])
|
|
109
|
+
// Casse : au moins une majuscule → la zone reste en place
|
|
110
|
+
if (char === char.toUpperCase()) zones[zone].fixed = true
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
for (const zone of Object.keys(zones) as LayoutZone[]) {
|
|
115
|
+
if (zones[zone].cells.length) {
|
|
116
|
+
const [r, c] = zones[zone].cells[0]!
|
|
117
|
+
zones[zone].area = areas[r]![c]!
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
view: rows.join(" "),
|
|
123
|
+
gridTemplateAreas: areas.map((row) => `"${row.join(" ")}"`).join(" "),
|
|
124
|
+
zones,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Vérifie qu'une zone occupe bien un rectangle plein (sinon `grid-template-areas` est invalide) */
|
|
129
|
+
function isRectangle(cells: Array<[number, number]>): boolean {
|
|
130
|
+
const rows = cells.map(([r]) => r)
|
|
131
|
+
const cols = cells.map(([, c]) => c)
|
|
132
|
+
const height = Math.max(...rows) - Math.min(...rows) + 1
|
|
133
|
+
const width = Math.max(...cols) - Math.min(...cols) + 1
|
|
134
|
+
return new Set(rows).size === height && new Set(cols).size === width && height * width === cells.length
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Vue par défaut + la raison du rejet, pour rester utilisable en production */
|
|
138
|
+
const fallback = (error: string): LayoutView => ({ ...build(DEFAULT_LAYOUT_VIEW), error })
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Analyse le prop `view`. Toute chaîne invalide est **rejetée** (jamais de crash) :
|
|
142
|
+
* la vue par défaut s'applique et `error` porte la raison (affichée en dev par le SFC).
|
|
143
|
+
*/
|
|
144
|
+
export function parseView(view?: string): LayoutView {
|
|
145
|
+
if (!view) return build(DEFAULT_LAYOUT_VIEW)
|
|
146
|
+
|
|
147
|
+
const groups = view.trim().split(/\s+/)
|
|
148
|
+
if (groups.length !== 3 || groups.some((group) => group.length !== 3)) {
|
|
149
|
+
return fallback(
|
|
150
|
+
`view "${view}" : attendu 3 groupes de 3 caractères (ex. "${DEFAULT_LAYOUT_VIEW}")`,
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const rows = groups as [string, string, string]
|
|
155
|
+
for (let r = 0; r < 3; r++) {
|
|
156
|
+
for (const char of rows[r]!) {
|
|
157
|
+
if (char === EMPTY_CELL) continue
|
|
158
|
+
const letter = char.toLowerCase()
|
|
159
|
+
if (!LETTER_ZONE[letter]) {
|
|
160
|
+
return fallback(`view "${view}" : lettre "${char}" inconnue (attendu l, h, r, f, p ou .)`)
|
|
161
|
+
}
|
|
162
|
+
if (!ROW_LETTERS[r]!.includes(letter)) {
|
|
163
|
+
return fallback(
|
|
164
|
+
`view "${view}" : "${char}" n'a pas sa place dans la rangée ${r + 1} (attendu ${ROW_LETTERS[r]!.join(", ")} ou .)`,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const parsed = build(rows.join(" "))
|
|
171
|
+
if (!parsed.zones.page.cells.length) {
|
|
172
|
+
return fallback(`view "${view}" : la rangée du milieu doit contenir au moins un "p" (page)`)
|
|
173
|
+
}
|
|
174
|
+
for (const zone of Object.keys(parsed.zones) as LayoutZone[]) {
|
|
175
|
+
const cells = parsed.zones[zone].cells
|
|
176
|
+
if (cells.length > 1 && !isRectangle(cells)) {
|
|
177
|
+
return fallback(`view "${view}" : la zone "${zone}" n'occupe pas un rectangle plein`)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return parsed
|
|
181
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Tests unitaires de la résolution du radius (échelle + style CSS).
|
|
2
|
+
import { describe, expect, it } from "bun:test"
|
|
3
|
+
import { RADIUS_VALUES, radiusStyle, radiusValue } from "./useComponentProps"
|
|
4
|
+
|
|
5
|
+
describe("radiusValue", () => {
|
|
6
|
+
it("traduit chaque barreau de l'échelle", () => {
|
|
7
|
+
expect(radiusValue("none")).toBe("0px")
|
|
8
|
+
expect(radiusValue("xs")).toBe("2px")
|
|
9
|
+
expect(radiusValue("sm")).toBe("4px")
|
|
10
|
+
expect(radiusValue("md")).toBe("8px")
|
|
11
|
+
expect(radiusValue("lg")).toBe("16px")
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it("ne traduit ni la forme native (true) ni l'absence de valeur", () => {
|
|
15
|
+
expect(radiusValue(true)).toBeUndefined()
|
|
16
|
+
expect(radiusValue(false)).toBeUndefined()
|
|
17
|
+
expect(radiusValue(undefined)).toBeUndefined()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("reste aligné sur RADIUS_VALUES", () => {
|
|
21
|
+
for (const [scale, value] of Object.entries(RADIUS_VALUES)) {
|
|
22
|
+
expect(radiusValue(scale as keyof typeof RADIUS_VALUES)).toBe(value)
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe("radiusStyle", () => {
|
|
28
|
+
it("pose --q-radius pour une valeur d'échelle", () => {
|
|
29
|
+
expect(radiusStyle("lg")).toEqual({ "--q-radius": "16px" })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("ne pose rien pour true (forme native) ou undefined", () => {
|
|
33
|
+
expect(radiusStyle(true)).toBeUndefined()
|
|
34
|
+
expect(radiusStyle(undefined)).toBeUndefined()
|
|
35
|
+
})
|
|
36
|
+
})
|
package/lib/useComponentProps.ts
CHANGED
|
@@ -55,7 +55,13 @@ export function useRadius(
|
|
|
55
55
|
return computed(() => toValue(own) ?? (defaults.value.radius as RadiusProp | undefined))
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** Valeur CSS concrète d'une échelle de radius (`undefined` si `true` (forme native) ou absent) */
|
|
59
|
+
export function radiusValue(radius: RadiusProp | undefined): string | undefined {
|
|
60
|
+
return isRadiusScale(radius) ? RADIUS_VALUES[radius] : undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
58
63
|
/** Style `--q-radius` à poser sur la racine du composant pour une valeur d'échelle. */
|
|
59
64
|
export function radiusStyle(radius: RadiusProp | undefined): Record<string, string> | undefined {
|
|
60
|
-
|
|
65
|
+
const value = radiusValue(radius)
|
|
66
|
+
return value ? { "--q-radius": value } : undefined
|
|
61
67
|
}
|
package/module.ts
CHANGED
|
@@ -20,15 +20,6 @@ export interface DnaxUiModuleOptions {
|
|
|
20
20
|
overlayBack?: boolean
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/** Custom elements du framework HTML Video.js v10 (@videojs/html) — balises non-Vue */
|
|
24
|
-
const VIDEOJS_CUSTOM_ELEMENTS = [
|
|
25
|
-
"video-player",
|
|
26
|
-
"video-skin",
|
|
27
|
-
"youtube-video",
|
|
28
|
-
"hlsjs-video",
|
|
29
|
-
"mux-video",
|
|
30
|
-
]
|
|
31
|
-
|
|
32
23
|
export default defineNuxtModule<DnaxUiModuleOptions>({
|
|
33
24
|
meta: {
|
|
34
25
|
name: "@dnax/ui",
|
|
@@ -43,15 +34,6 @@ export default defineNuxtModule<DnaxUiModuleOptions>({
|
|
|
43
34
|
setup(options, nuxt) {
|
|
44
35
|
const { resolve } = createResolver(import.meta.url)
|
|
45
36
|
|
|
46
|
-
// Les balises <video-player>/<youtube-video>… sont des custom elements @videojs/html,
|
|
47
|
-
// pas des composants Vue → les exclure de la résolution (évite le warn
|
|
48
|
-
// « Failed to resolve component »).
|
|
49
|
-
nuxt.options.vue.compilerOptions ??= {}
|
|
50
|
-
const existing = nuxt.options.vue.compilerOptions.isCustomElement
|
|
51
|
-
nuxt.options.vue.compilerOptions.isCustomElement = (tag: string) =>
|
|
52
|
-
VIDEOJS_CUSTOM_ELEMENTS.includes(tag) ||
|
|
53
|
-
(typeof existing === "function" ? existing(tag) : false)
|
|
54
|
-
|
|
55
37
|
if (options.css) {
|
|
56
38
|
const cssPath = resolve("./styles/main.css")
|
|
57
39
|
if (!nuxt.options.css.includes(cssPath)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dnax/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.46",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"description": "UI design System",
|
|
6
6
|
"type": "module",
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"@tiptap/starter-kit": "^3.31.3",
|
|
41
41
|
"@tiptap/suggestion": "^3.31.3",
|
|
42
42
|
"@tiptap/vue-3": "^3.31.3",
|
|
43
|
-
"@videojs/html": "^10.0.0-beta.32",
|
|
44
43
|
"@vueuse/core": "^14.4.0",
|
|
45
44
|
"arktype": "^2.2.3",
|
|
46
45
|
"class-variance-authority": "^0.7.1",
|
|
@@ -54,6 +54,15 @@ export {
|
|
|
54
54
|
} from "./lib/overlayBack"
|
|
55
55
|
export type { OverlayHandle } from "./lib/overlayBack"
|
|
56
56
|
export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
|
|
57
|
+
export { placePopover } from "./lib/datePicker"
|
|
58
|
+
export type {
|
|
59
|
+
PlacePopoverOptions,
|
|
60
|
+
PopoverAnchor,
|
|
61
|
+
PopoverPlacement,
|
|
62
|
+
PopoverViewport,
|
|
63
|
+
} from "./lib/datePicker"
|
|
64
|
+
export { DEFAULT_LAYOUT_VIEW, parseView, qLayoutKey } from "./lib/layout"
|
|
65
|
+
export type { LayoutView, LayoutZone, LayoutZonePlacement, QLayoutContext } from "./lib/layout"
|
|
57
66
|
export { vRipple } from "./lib/ripple"
|
|
58
67
|
export type { RippleOptions, RippleValue } from "./lib/ripple"
|
|
59
68
|
export { chartToECharts, channelValue, pickFromEvent, tableModel, textValue } from "./lib/chart"
|
package/styles/main.css
CHANGED
|
@@ -32,18 +32,22 @@ body {
|
|
|
32
32
|
/* ===== Échelle des z-index =====
|
|
33
33
|
Règle : un menu superposé ne doit JAMAIS passer sous un overlay. Les menus
|
|
34
34
|
téléportés dans <body> sont donc au-dessus de la couche modale.
|
|
35
|
-
1000 popups en flux (`position: absolute` dans leur champ :
|
|
36
|
-
|
|
35
|
+
1000 popups en flux (`position: absolute` dans leur champ : autocomplete,
|
|
36
|
+
nav-menu…) → pas concernés par les overlays
|
|
37
37
|
1999/2000 sidebar offcanvas + backdrop
|
|
38
38
|
3000 overlays modaux : dialog, bottom sheet, action sheet,
|
|
39
39
|
date-picker, country-picker, select sheet
|
|
40
40
|
3200 menus téléportés dans <body> : panneau QBtnActions, menu
|
|
41
41
|
contextuel / filtre / suggestions du tableur → --q-z-menu
|
|
42
|
+
3500 popup inline de QSelect (téléporté dans <body> : il doit passer
|
|
43
|
+
au-dessus d'un overlay quand le champ est dans un dialog/sheet)
|
|
44
|
+
→ --q-z-popup
|
|
42
45
|
4000 palettes tiptap · 7000 loading global · 9999 tooltip ·
|
|
43
46
|
10000 image preview
|
|
44
|
-
Surchargez --q-z-menu si votre application a un overlay plus haut
|
|
47
|
+
Surchargez --q-z-menu ou --q-z-popup si votre application a un overlay plus haut. */
|
|
45
48
|
:root {
|
|
46
49
|
--q-z-menu: 3200;
|
|
50
|
+
--q-z-popup: 3500;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/* ===== Safe-area insets (encoche & barre d'accueil) =====
|
|
@@ -52,8 +56,13 @@ body {
|
|
|
52
56
|
.q-app {
|
|
53
57
|
display: flex;
|
|
54
58
|
flex-direction: column; /* header en haut, contenu au milieu, footer en bas */
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
width: 100%; /* prend toute la largeur disponible */
|
|
60
|
+
/* Équivalent PUR CSS de `flex h-screen w-full` (les classes sont aussi sur la
|
|
61
|
+
div) : le conteneur fait exactement un écran. Ce qui dépasse doit donc défiler
|
|
62
|
+
dans un enfant (`min-height: 0` + `overflow: auto`), sinon le document scrolle
|
|
63
|
+
sous les barres. */
|
|
64
|
+
height: 100vh; /* fallback vieux navigateurs */
|
|
65
|
+
height: 100dvh; /* barres de navigateur dynamiques (mobile) */
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
/* Safe-area intégrées aux règles de base (pas de conflit de cascade) */
|
|
@@ -79,6 +88,16 @@ body {
|
|
|
79
88
|
z-index: 100;
|
|
80
89
|
}
|
|
81
90
|
|
|
91
|
+
/* sticky : opt-in (prop `sticky`, ou automatique dans un QLayout quand la lettre du
|
|
92
|
+
`view` est en majuscule) — la barre reste visible, le contenu défile dessous.
|
|
93
|
+
Dans un QLayout elle est close dans sa cellule : elle ne peut pas passer au-dessus
|
|
94
|
+
d'une colonne voisine (drawer, rangée du milieu). */
|
|
95
|
+
.q-header--sticky {
|
|
96
|
+
position: sticky;
|
|
97
|
+
top: 0;
|
|
98
|
+
z-index: 100;
|
|
99
|
+
}
|
|
100
|
+
|
|
82
101
|
/* Barre d'action basse */
|
|
83
102
|
/* Safe-area intégrées à la règle de base .q-footer (plus bas) */
|
|
84
103
|
|
|
@@ -105,6 +124,49 @@ body {
|
|
|
105
124
|
padding-bottom: calc(env(safe-area-inset-bottom) + 50px);
|
|
106
125
|
}
|
|
107
126
|
|
|
127
|
+
/* ===== QLayout — matrice 3×3 pilotée par `view` =====
|
|
128
|
+
Colonnes : drawer gauche / contenu / drawer droit. Rangées : header / milieu /
|
|
129
|
+
footer. `grid-template-areas` vient du prop `view` (variable CSS posée par le
|
|
130
|
+
composant). `minmax(0, 1fr)` sur la colonne du milieu = l'équivalent de
|
|
131
|
+
`min-width: 0` : un enfant large (tableau à colonnes nombreuses) ne pousse plus
|
|
132
|
+
les drawers hors de l'écran. */
|
|
133
|
+
.q-layout {
|
|
134
|
+
display: grid;
|
|
135
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
136
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
137
|
+
grid-template-areas: var(--q-layout-areas);
|
|
138
|
+
min-height: 100vh;
|
|
139
|
+
min-height: 100dvh;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Tout enfant direct sans cellule explicite (le contenu) va dans la cellule « page ».
|
|
143
|
+
q-header / q-footer / q-sidebar posent la leur en style inline — l'ordre
|
|
144
|
+
d'écriture dans le template est donc libre. */
|
|
145
|
+
.q-layout > * {
|
|
146
|
+
grid-area: var(--q-layout-page-area, p);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Mode conteneur (comme Quasar) : le layout gère son propre cadre — une hauteur
|
|
150
|
+
explicite est requise et le conteneur devient la zone de scroll (les barres
|
|
151
|
+
sticky s'y accrochent, pas à la fenêtre). */
|
|
152
|
+
.q-layout--container {
|
|
153
|
+
min-height: 0;
|
|
154
|
+
overflow: auto;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* Drawer sticky dans un QLayout : accroché sous les barres qui le surplombent
|
|
158
|
+
seulement (les variables `--q-sidebar-sticky-*` sont posées par QSidebar d'après
|
|
159
|
+
les cellules de sa zone), sur la hauteur visible entre les deux barres — il ne
|
|
160
|
+
glisse donc jamais sous le header. */
|
|
161
|
+
.q-layout > .q-sidebar--static.q-sidebar--sticky {
|
|
162
|
+
top: var(--q-sidebar-sticky-top, var(--q-layout-header-h, 0px));
|
|
163
|
+
height: calc(
|
|
164
|
+
var(--q-layout-viewport-h, 100dvh) -
|
|
165
|
+
var(--q-sidebar-sticky-top, var(--q-layout-header-h, 0px)) -
|
|
166
|
+
var(--q-sidebar-sticky-bottom, var(--q-layout-footer-h, 0px))
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
108
170
|
.q-toolbar {
|
|
109
171
|
position: relative;
|
|
110
172
|
display: flex;
|
|
@@ -258,7 +320,10 @@ body {
|
|
|
258
320
|
--muted: #1c2128;
|
|
259
321
|
--border: rgb(255 255 255 / 0.12);
|
|
260
322
|
--q-table-border: rgb(255 255 255 / 0.12);
|
|
261
|
-
|
|
323
|
+
/* Opaque (4 % de blanc composé sur la surface) : un en-tête sticky ne doit pas
|
|
324
|
+
laisser voir les lignes qui défilent dessous. Fallback avant color-mix. */
|
|
325
|
+
--q-table-header-bg: #1f242b;
|
|
326
|
+
--q-table-header-bg: color-mix(in srgb, #fff 4%, var(--card, #161b22));
|
|
262
327
|
--q-table-hover-bg: rgb(255 255 255 / 0.06);
|
|
263
328
|
--q-sidebar-border: rgb(255 255 255 / 0.12);
|
|
264
329
|
--q-translucent-bg: #0f1115;
|
|
@@ -352,7 +417,9 @@ html.dark body {
|
|
|
352
417
|
}
|
|
353
418
|
.dark .q-data-grid {
|
|
354
419
|
--q-data-grid-border: rgb(255 255 255 / 0.12);
|
|
355
|
-
|
|
420
|
+
/* Opaque (cf. QTable : en-tête sticky sur surface composée) */
|
|
421
|
+
--q-data-grid-header-bg: #1f242b;
|
|
422
|
+
--q-data-grid-header-bg: color-mix(in srgb, #fff 4%, var(--card, #161b22));
|
|
356
423
|
}
|
|
357
424
|
|
|
358
425
|
/* Footer de table (pagination) en dark : range + sélecteur lignes/page */
|
|
@@ -653,6 +720,15 @@ html.dark body {
|
|
|
653
720
|
z-index: 100;
|
|
654
721
|
}
|
|
655
722
|
|
|
723
|
+
/* sticky (prop `sticky`, ou automatique dans un QLayout avec une lettre majuscule) :
|
|
724
|
+
la rangée footer étant la dernière, elle tombe en fin de page dans les deux cas —
|
|
725
|
+
`F` ne change donc rien de visible, contrairement à `H` sur le header. */
|
|
726
|
+
.q-footer--sticky {
|
|
727
|
+
position: sticky;
|
|
728
|
+
bottom: 0;
|
|
729
|
+
z-index: 100;
|
|
730
|
+
}
|
|
731
|
+
|
|
656
732
|
/* no-padding : horizontal collé aux bords — safe-area conservées */
|
|
657
733
|
.q-footer--no-padding {
|
|
658
734
|
padding: 4px 0;
|
|
@@ -3142,7 +3218,10 @@ html.dark body {
|
|
|
3142
3218
|
/* ===== QTable ===== */
|
|
3143
3219
|
.q-table {
|
|
3144
3220
|
--q-table-border: rgb(0 0 0 / 0.1);
|
|
3145
|
-
|
|
3221
|
+
/* Opaque : 3 % de noir **composé sur la surface** (un en-tête sticky laissait
|
|
3222
|
+
sinon voir les lignes qui défilent dessous). Fallback avant color-mix. */
|
|
3223
|
+
--q-table-header-bg: #f7f7f7;
|
|
3224
|
+
--q-table-header-bg: color-mix(in srgb, #000 3%, #fff);
|
|
3146
3225
|
--q-table-hover-bg: rgb(0 0 0 / 0.04);
|
|
3147
3226
|
|
|
3148
3227
|
position: relative;
|
|
@@ -3166,7 +3245,8 @@ html.dark body {
|
|
|
3166
3245
|
|
|
3167
3246
|
.q-table--dark {
|
|
3168
3247
|
--q-table-border: rgb(255 255 255 / 0.15);
|
|
3169
|
-
--q-table-header-bg:
|
|
3248
|
+
--q-table-header-bg: #2b2b2b;
|
|
3249
|
+
--q-table-header-bg: color-mix(in srgb, #fff 6%, #1d1d1d);
|
|
3170
3250
|
--q-table-hover-bg: rgb(255 255 255 / 0.06);
|
|
3171
3251
|
background-color: #1d1d1d;
|
|
3172
3252
|
color: #fff;
|
|
@@ -4768,12 +4848,16 @@ a.q-breadcrumbs__el:hover {
|
|
|
4768
4848
|
transform: rotate(180deg);
|
|
4769
4849
|
}
|
|
4770
4850
|
|
|
4851
|
+
/* Popup inline : téléporté dans <body> et en `position: fixed` — en `position:
|
|
4852
|
+
absolute` il restait **rogné** par le premier ancêtre en `overflow: hidden` (un
|
|
4853
|
+
q-card, par exemple), et un `z-index` ne franchit JAMAIS un clip. Les coordonnées
|
|
4854
|
+
fenêtre (top/bottom/left/width) sont posées en inline par `positionPopup()`, qui
|
|
4855
|
+
le repositionne aussi au scroll et au resize. */
|
|
4771
4856
|
.q-select__popup {
|
|
4772
|
-
position:
|
|
4773
|
-
top:
|
|
4857
|
+
position: fixed;
|
|
4858
|
+
top: 0;
|
|
4774
4859
|
left: 0;
|
|
4775
|
-
|
|
4776
|
-
z-index: 1000;
|
|
4860
|
+
z-index: var(--q-z-popup, 3500);
|
|
4777
4861
|
max-height: 240px;
|
|
4778
4862
|
overflow-y: auto;
|
|
4779
4863
|
padding: 4px 0;
|
|
@@ -8581,7 +8665,9 @@ a.q-breadcrumbs__el:hover {
|
|
|
8581
8665
|
/* ===== QDataGrid — grille type tableur (formules, édition, sélection clavier) ===== */
|
|
8582
8666
|
.q-data-grid {
|
|
8583
8667
|
--q-data-grid-border: rgb(0 0 0 / 0.1);
|
|
8584
|
-
|
|
8668
|
+
/* Opaque (cf. QTable : en-tête sticky sur surface composée) */
|
|
8669
|
+
--q-data-grid-header-bg: #f7f7f7;
|
|
8670
|
+
--q-data-grid-header-bg: color-mix(in srgb, #000 3%, #fff);
|
|
8585
8671
|
|
|
8586
8672
|
position: relative;
|
|
8587
8673
|
display: flex;
|
|
@@ -8600,7 +8686,8 @@ a.q-breadcrumbs__el:hover {
|
|
|
8600
8686
|
}
|
|
8601
8687
|
.q-data-grid--dark {
|
|
8602
8688
|
--q-data-grid-border: rgb(255 255 255 / 0.15);
|
|
8603
|
-
--q-data-grid-header-bg:
|
|
8689
|
+
--q-data-grid-header-bg: #2b2b2b;
|
|
8690
|
+
--q-data-grid-header-bg: color-mix(in srgb, #fff 6%, #1d1d1d);
|
|
8604
8691
|
background: #1d1d1d;
|
|
8605
8692
|
color: #fff;
|
|
8606
8693
|
}
|
|
@@ -9516,11 +9603,13 @@ a.q-breadcrumbs__el:hover {
|
|
|
9516
9603
|
width: 14px;
|
|
9517
9604
|
height: 14px;
|
|
9518
9605
|
}
|
|
9519
|
-
|
|
9520
|
-
|
|
9606
|
+
/* Popup inline téléporté dans <body> : impossible de le styler depuis le parent — on
|
|
9607
|
+
passe par la classe du popup (`inline-options.class`) ; la largeur, elle, vient de
|
|
9608
|
+
`inline-options.width` (l'inline posé par positionPopup() prime sur ce CSS). */
|
|
9609
|
+
.q-tiptap__font-size-popup {
|
|
9521
9610
|
min-width: 100px;
|
|
9522
9611
|
}
|
|
9523
|
-
.q-tiptap__font-size .q-select__option-label {
|
|
9612
|
+
.q-tiptap__font-size-popup .q-select__option-label {
|
|
9524
9613
|
font-size: 12px;
|
|
9525
9614
|
}
|
|
9526
9615
|
|
|
@@ -9710,7 +9799,9 @@ a.q-breadcrumbs__el:hover {
|
|
|
9710
9799
|
/* ===== QSpreadsheet (tableur type Excel) ===== */
|
|
9711
9800
|
.q-spreadsheet {
|
|
9712
9801
|
--q-spreadsheet-border: rgb(0 0 0 / 0.1);
|
|
9713
|
-
|
|
9802
|
+
/* Opaque (cf. QTable : entêtes sticky des colonnes/lignes) */
|
|
9803
|
+
--q-spreadsheet-header-bg: #f7f7f7;
|
|
9804
|
+
--q-spreadsheet-header-bg: color-mix(in srgb, #000 3%, #fff);
|
|
9714
9805
|
--q-spreadsheet-row-alt: rgb(0 0 0 / 0.015);
|
|
9715
9806
|
--q-spreadsheet-range: rgb(25 118 210 / 0.1);
|
|
9716
9807
|
--q-spreadsheet-range-brd: var(--primary, #1976d2);
|
|
@@ -9739,7 +9830,8 @@ a.q-breadcrumbs__el:hover {
|
|
|
9739
9830
|
.q-spreadsheet--dark,
|
|
9740
9831
|
.dark .q-spreadsheet {
|
|
9741
9832
|
--q-spreadsheet-border: rgb(255 255 255 / 0.12);
|
|
9742
|
-
--q-spreadsheet-header-bg:
|
|
9833
|
+
--q-spreadsheet-header-bg: #22262d;
|
|
9834
|
+
--q-spreadsheet-header-bg: color-mix(in srgb, #fff 5%, var(--card, #1d1d1d));
|
|
9743
9835
|
--q-spreadsheet-row-alt: rgb(255 255 255 / 0.02);
|
|
9744
9836
|
background: var(--card, #1d1d1d);
|
|
9745
9837
|
color: var(--foreground, #ececef);
|