@dnax/ui 0.0.43 → 0.0.44
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/QChart.vue +533 -0
- package/components/QDatePicker.vue +95 -14
- package/components/internal/QDateCalendar.vue +112 -19
- package/index.ts +21 -1
- package/lib/chart.test.ts +991 -0
- package/lib/chart.ts +1340 -0
- package/lib/color.test.ts +81 -0
- package/lib/color.ts +60 -0
- package/lib/datePicker.test.ts +122 -0
- package/lib/datePicker.ts +114 -0
- package/package.json +2 -9
- package/scripts/generate-exports.ts +13 -0
- package/styles/main.css +251 -130
- package/components/QEditorJs.vue +0 -146
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Tests unitaires de la normalisation des couleurs (bun test).
|
|
2
|
+
import { describe, expect, it } from "bun:test"
|
|
3
|
+
import { normalizeCssColor, rgbaFromBytes } from "./color"
|
|
4
|
+
|
|
5
|
+
/** Faux contexte 2D mimant un navigateur : une valeur **non reconnue** laisse `fillStyle`
|
|
6
|
+
* inchangé (c'est cette règle qui sert à détecter une couleur invalide). */
|
|
7
|
+
function fakeCtx(options: {
|
|
8
|
+
/** Sérialisation de la couleur (Chrome conserve l'espace : `oklch(…)` reste `oklch(…)`) */
|
|
9
|
+
serialize?: (value: string) => string
|
|
10
|
+
/** Lecture du pixel peint */
|
|
11
|
+
pixel?: [number, number, number, number]
|
|
12
|
+
/** Reconnaissance d'une couleur CSS */
|
|
13
|
+
recognized?: (value: string) => boolean
|
|
14
|
+
}) {
|
|
15
|
+
const {
|
|
16
|
+
serialize = (v: string) => v,
|
|
17
|
+
pixel = [249, 115, 22, 255] as [number, number, number, number],
|
|
18
|
+
recognized = (v: string) => v.trim() !== "" && !/^zorglub/.test(v),
|
|
19
|
+
} = options
|
|
20
|
+
const ctx = {
|
|
21
|
+
_fill: "#000000",
|
|
22
|
+
get fillStyle() {
|
|
23
|
+
return this._fill
|
|
24
|
+
},
|
|
25
|
+
set fillStyle(value: string) {
|
|
26
|
+
if (recognized(value)) this._fill = serialize(value)
|
|
27
|
+
},
|
|
28
|
+
clearRect() {},
|
|
29
|
+
fillRect() {},
|
|
30
|
+
getImageData: () => ({ data: Uint8ClampedArray.from(pixel) }),
|
|
31
|
+
}
|
|
32
|
+
return ctx as unknown as CanvasRenderingContext2D
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("rgbaFromBytes", () => {
|
|
36
|
+
it("rend `#rrggbb` pour une couleur opaque", () => {
|
|
37
|
+
expect(rgbaFromBytes(249, 115, 22, 255)).toBe("#f97316")
|
|
38
|
+
expect(rgbaFromBytes(0, 0, 0, 255)).toBe("#000000")
|
|
39
|
+
})
|
|
40
|
+
it("rend `rgba()` dès qu'il y a de la transparence", () => {
|
|
41
|
+
expect(rgbaFromBytes(0, 0, 0, 0)).toBe("rgba(0, 0, 0, 0)")
|
|
42
|
+
expect(rgbaFromBytes(0, 0, 0, 140)).toBe("rgba(0, 0, 0, 0.549)")
|
|
43
|
+
expect(rgbaFromBytes(255, 255, 255, 128)).toBe("rgba(255, 255, 255, 0.502)")
|
|
44
|
+
})
|
|
45
|
+
it("borne les octets", () => {
|
|
46
|
+
expect(rgbaFromBytes(-20, 300, 12.6, 255)).toBe("#00ff0d")
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe("normalizeCssColor", () => {
|
|
51
|
+
it("convertit un oklch alors même que `fillStyle` conserve l'espace colorimétrique", () => {
|
|
52
|
+
// Comportement de Chrome : la sérialisation garde `oklch(…)` — d'où le bug de survol
|
|
53
|
+
// quand on recopiait simplement `fillStyle` (zrender ne sait pas relire un oklch).
|
|
54
|
+
const ctx = fakeCtx({ serialize: (v) => (v.startsWith("oklch") ? "oklch(0.646 0.222 41.116)" : v) })
|
|
55
|
+
expect(normalizeCssColor("oklch(64.6% .222 41.116)", ctx)).toBe("#f97316")
|
|
56
|
+
})
|
|
57
|
+
it("convertit les autres syntaxes modernes et garde les formes simples", () => {
|
|
58
|
+
const ctx = fakeCtx({ serialize: (v) => (v.startsWith("oklch") ? "oklch(0.5 0.1 200)" : v) })
|
|
59
|
+
expect(normalizeCssColor("#f97316", ctx)).toBe("#f97316")
|
|
60
|
+
expect(normalizeCssColor("color-mix(in srgb, #fff 70%, transparent)", ctx)).toBe("#f97316")
|
|
61
|
+
expect(normalizeCssColor("rgb(0 0 0 / 0.55)", ctx)).toBe("#f97316")
|
|
62
|
+
})
|
|
63
|
+
it("conserve la transparence relue sur le pixel", () => {
|
|
64
|
+
const ctx = fakeCtx({ serialize: (v) => (v.startsWith("oklch") ? "oklch(0.5 0.1 200)" : v), pixel: [0, 0, 0, 140] })
|
|
65
|
+
expect(normalizeCssColor("oklch(50% .1 200 / 55%)", ctx)).toBe("rgba(0, 0, 0, 0.549)")
|
|
66
|
+
})
|
|
67
|
+
it("renvoie `undefined` pour une couleur invalide (ou un contexte absent)", () => {
|
|
68
|
+
expect(normalizeCssColor("zorglub(1)", fakeCtx({}))).toBeUndefined()
|
|
69
|
+
expect(normalizeCssColor("", fakeCtx({}))).toBeUndefined()
|
|
70
|
+
expect(normalizeCssColor({ toString: () => "#fff" } as unknown as string, fakeCtx({}))).toBeUndefined()
|
|
71
|
+
expect(normalizeCssColor("oklch(1 0 0)", null)).toBeUndefined()
|
|
72
|
+
expect(normalizeCssColor("oklch(1 0 0)", undefined)).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
it("retombe sur la sérialisation si la lecture des pixels échoue", () => {
|
|
75
|
+
const ctx = fakeCtx({ serialize: (v) => (v.startsWith("oklch") ? "oklch(0.5 0.1 200)" : v) })
|
|
76
|
+
;(ctx as any).getImageData = () => {
|
|
77
|
+
throw new Error("canvas illisible")
|
|
78
|
+
}
|
|
79
|
+
expect(normalizeCssColor("oklch(50% .1 200)", ctx)).toBe("oklch(0.5 0.1 200)")
|
|
80
|
+
})
|
|
81
|
+
})
|
package/lib/color.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Normalisation des couleurs CSS → une forme que **zrender** (le moteur d'ECharts) sait
|
|
2
|
+
// relire : `#rrggbb`, `rgb(a,b,c)`, `rgba(a,b,c,a)`, `hsl(h,s%,l%)` ou un nom CSS.
|
|
3
|
+
//
|
|
4
|
+
// Pourquoi c'est vital : au survol, ECharts **recalcule** la couleur de l'élément —
|
|
5
|
+
// `emphasisStyle.fill = liftColor(fromFill)` (`echarts/lib/util/states.js`)
|
|
6
|
+
// — et `liftColor()` s'appuie sur le parseur de zrender. Sur une couleur moderne
|
|
7
|
+
// (`oklch()`, `color-mix()`, `rgb(0 0 0 / .5)` — c'est-à-dire **tous** les tokens d'un
|
|
8
|
+
// thème shadcn/Docd) la fonction sort sans `return` → `undefined` → la forme est dessinée
|
|
9
|
+
// sans remplissage → **l'élément survolé disparaît** (cf. `.memory/warnings.md`).
|
|
10
|
+
//
|
|
11
|
+
// Piège vérifié : `ctx.fillStyle = "oklch(…)"` **conserve l'espace colorimétrique** — la
|
|
12
|
+
// relecture rend la même chaîne `oklch(…)`. Recopier `fillStyle` ne normalise donc rien ;
|
|
13
|
+
// le seul moyen portable de convertir n'importe quelle couleur CSS en sRGB est de la
|
|
14
|
+
// **peindre puis de relire les pixels** (`getImageData`).
|
|
15
|
+
|
|
16
|
+
/** Sentinelles : une couleur invalide laisse `fillStyle` inchangé */
|
|
17
|
+
const SENTINELS = ["#010203", "#040506"]
|
|
18
|
+
|
|
19
|
+
/** Octets sRGB → CSS relisible par zrender : `#rrggbb` si opaque, sinon `rgba(…)` */
|
|
20
|
+
export function rgbaFromBytes(r: number, g: number, b: number, a: number): string {
|
|
21
|
+
const clamp = (n: number) => Math.max(0, Math.min(255, Math.round(n)))
|
|
22
|
+
const byte = (n: number) => clamp(n).toString(16).padStart(2, "0")
|
|
23
|
+
// Opaque → `#rrggbb` (la forme la plus lisible) ; sinon l'alpha est conservé
|
|
24
|
+
return clamp(a) === 255
|
|
25
|
+
? `#${byte(r)}${byte(g)}${byte(b)}`
|
|
26
|
+
: `rgba(${clamp(r)}, ${clamp(g)}, ${clamp(b)}, ${+(clamp(a) / 255).toFixed(3)})`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Couleur CSS → `#rrggbb` / `rgba(…)`, ou `undefined` si elle est invalide (ou sans
|
|
30
|
+
* contexte). `ctx` doit être un contexte 2D **1×1** : le canvas ne sert qu'à ça. */
|
|
31
|
+
export function normalizeCssColor(
|
|
32
|
+
value: any,
|
|
33
|
+
ctx: CanvasRenderingContext2D | null | undefined,
|
|
34
|
+
): string | undefined {
|
|
35
|
+
if (!ctx || typeof value !== "string" || value.trim() === "") return undefined
|
|
36
|
+
// La couleur est-elle reconnue par le navigateur ? (`fillStyle` inchangé = invalide)
|
|
37
|
+
let known = false
|
|
38
|
+
for (const sentinel of SENTINELS) {
|
|
39
|
+
ctx.fillStyle = sentinel
|
|
40
|
+
ctx.fillStyle = value
|
|
41
|
+
if (ctx.fillStyle !== sentinel) {
|
|
42
|
+
known = true
|
|
43
|
+
break
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!known) return undefined
|
|
47
|
+
try {
|
|
48
|
+
// Peindre la couleur puis relire le pixel : conversion **indépendante de l'espace
|
|
49
|
+
// colorimétrique** (le seul moyen de sortir un oklch/color-mix en sRGB).
|
|
50
|
+
ctx.clearRect(0, 0, 1, 1)
|
|
51
|
+
ctx.fillStyle = value
|
|
52
|
+
ctx.fillRect(0, 0, 1, 1)
|
|
53
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data
|
|
54
|
+
return rgbaFromBytes(r ?? 0, g ?? 0, b ?? 0, a ?? 0)
|
|
55
|
+
} catch {
|
|
56
|
+
// Canvas illisible (contexte perdu, rendu logiciel…) : on retombe sur la
|
|
57
|
+
// sérialisation du navigateur — imparfaite, mais c'est le comportement historique.
|
|
58
|
+
return typeof ctx.fillStyle === "string" ? ctx.fillStyle : undefined
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Tests unitaires du placement du panneau ancré (mode `popover` de QDatePicker).
|
|
2
|
+
import { describe, expect, it } from "bun:test"
|
|
3
|
+
import {
|
|
4
|
+
POPOVER_CARET_INSET,
|
|
5
|
+
POPOVER_FALLBACK_WIDTH,
|
|
6
|
+
POPOVER_GAP,
|
|
7
|
+
POPOVER_VIEWPORT_MARGIN,
|
|
8
|
+
placePopover,
|
|
9
|
+
} from "./datePicker"
|
|
10
|
+
|
|
11
|
+
const viewport = { width: 1280, height: 800 }
|
|
12
|
+
|
|
13
|
+
/** Champ de 320px de large, centré horizontalement */
|
|
14
|
+
const anchorAt = (top: number, left = 400, width = 320) => ({
|
|
15
|
+
top,
|
|
16
|
+
bottom: top + 40,
|
|
17
|
+
left,
|
|
18
|
+
width,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe("placePopover", () => {
|
|
22
|
+
it("ouvre sous le champ quand la place est suffisante", () => {
|
|
23
|
+
const p = placePopover({ anchor: anchorAt(100), viewport })
|
|
24
|
+
|
|
25
|
+
expect(p.direction).toBe("down")
|
|
26
|
+
expect(p.top).toBe(140 + POPOVER_GAP)
|
|
27
|
+
expect(p.bottom).toBeNull()
|
|
28
|
+
// 800 - 140 - 8 = 652 px visibles sous le champ, moins l'écart
|
|
29
|
+
expect(p.maxHeight).toBe(800 - 140 - POPOVER_VIEWPORT_MARGIN - POPOVER_GAP)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("bascule au-dessus du champ quand le bas manque de place", () => {
|
|
33
|
+
const p = placePopover({ anchor: anchorAt(700), viewport })
|
|
34
|
+
|
|
35
|
+
expect(p.direction).toBe("up")
|
|
36
|
+
expect(p.top).toBeNull()
|
|
37
|
+
// Bord bas : distance entre le bas de la fenêtre et le haut du champ, plus l'écart
|
|
38
|
+
expect(p.bottom).toBe(800 - 700 + POPOVER_GAP)
|
|
39
|
+
expect(p.maxHeight).toBe(700 - POPOVER_VIEWPORT_MARGIN - POPOVER_GAP)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("reste en dessous quand les deux côtés sont à égalité", () => {
|
|
43
|
+
// 380px de chaque côté : `below >= above` tranche pour le bas
|
|
44
|
+
const p = placePopover({ anchor: anchorAt(380), viewport })
|
|
45
|
+
expect(p.direction).toBe("down")
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("recadre le panneau dans la fenêtre sur un champ collé à droite", () => {
|
|
49
|
+
const p = placePopover({
|
|
50
|
+
anchor: anchorAt(100, 1150, 320),
|
|
51
|
+
viewport,
|
|
52
|
+
panelWidth: 360,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// 1280 - 360 - 8 = 912 : le panneau ne dépasse pas le bord droit
|
|
56
|
+
expect(p.left).toBe(912)
|
|
57
|
+
// La flèche suit le centre du champ, ramenée à 16px du coin droit
|
|
58
|
+
expect(p.caret).toBe(360 - POPOVER_CARET_INSET)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("colle le panneau au bord gauche et suit le centre du champ", () => {
|
|
62
|
+
const p = placePopover({
|
|
63
|
+
anchor: anchorAt(100, 0, 200),
|
|
64
|
+
viewport,
|
|
65
|
+
panelWidth: 360,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
expect(p.left).toBe(POPOVER_VIEWPORT_MARGIN)
|
|
69
|
+
// Centre du champ (100) → 92px depuis le bord gauche du panneau (8)
|
|
70
|
+
expect(p.caret).toBe(100 - POPOVER_VIEWPORT_MARGIN)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("ramène la flèche dans le panneau quand le champ sort par la gauche", () => {
|
|
74
|
+
const p = placePopover({
|
|
75
|
+
anchor: anchorAt(100, -500, 200),
|
|
76
|
+
viewport,
|
|
77
|
+
panelWidth: 360,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect(p.left).toBe(POPOVER_VIEWPORT_MARGIN)
|
|
81
|
+
expect(p.caret).toBe(POPOVER_CARET_INSET)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("utilise la largeur de repli tant que le panneau n'est pas mesuré", () => {
|
|
85
|
+
const p = placePopover({ anchor: anchorAt(100), viewport })
|
|
86
|
+
expect(p.left + POPOVER_FALLBACK_WIDTH).toBeLessThanOrEqual(
|
|
87
|
+
viewport.width - POPOVER_VIEWPORT_MARGIN,
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("borne la largeur à la fenêtre et garde la flèche dans le panneau", () => {
|
|
92
|
+
const p = placePopover({
|
|
93
|
+
anchor: anchorAt(100, 0, 300),
|
|
94
|
+
viewport: { width: 320, height: 800 },
|
|
95
|
+
panelWidth: 520,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Largeur utile = 320 - 2 × 8 = 304
|
|
99
|
+
expect(p.left).toBe(POPOVER_VIEWPORT_MARGIN)
|
|
100
|
+
expect(p.caret).toBeGreaterThanOrEqual(POPOVER_CARET_INSET)
|
|
101
|
+
expect(p.caret).toBeLessThanOrEqual(304 - POPOVER_CARET_INSET)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("réduit l'écart et borne la hauteur quand la place manque en bas", () => {
|
|
105
|
+
// Fenêtre courte : 202px sous le champ (< 220px visés), plus qu'au-dessus
|
|
106
|
+
const p = placePopover({ anchor: anchorAt(170), viewport: { width: 1280, height: 420 }, gap: 24 })
|
|
107
|
+
|
|
108
|
+
expect(p.direction).toBe("down")
|
|
109
|
+
// L'écart demandé (24px) est rogné : plus de place au panneau
|
|
110
|
+
expect(p.top).toBe(210)
|
|
111
|
+
expect(p.maxHeight).toBe(420 - 210 - POPOVER_VIEWPORT_MARGIN)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("ne renvoie jamais une hauteur négative", () => {
|
|
115
|
+
// Fenêtre plus courte que les marges : tout est dégénéré mais reste utilisable
|
|
116
|
+
const p = placePopover({ anchor: anchorAt(0), viewport: { width: 8, height: 0 } })
|
|
117
|
+
|
|
118
|
+
expect(p.maxHeight).toBeGreaterThanOrEqual(0)
|
|
119
|
+
expect(p.left).toBe(POPOVER_VIEWPORT_MARGIN)
|
|
120
|
+
expect(p.caret).toBeGreaterThanOrEqual(POPOVER_CARET_INSET)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Placement pur d'un panneau ancré à un champ — mode `popover` de QDatePicker.
|
|
2
|
+
//
|
|
3
|
+
// Aucune dépendance au DOM : le SFC mesure les rectangles (champ + fenêtre) et
|
|
4
|
+
// sérialise le résultat en `position: fixed` ; ce module décide **où**. La règle est
|
|
5
|
+
// la même que celle du popup `inline` de QSelect : ouverture vers le bas, bascule
|
|
6
|
+
// au-dessus quand la place manque, écart réduit à mesure que la place se réduit, et
|
|
7
|
+
// hauteur bornée à l'espace visible (le panneau scrolle au-delà).
|
|
8
|
+
|
|
9
|
+
/** Marge conservée avec les bords de la fenêtre */
|
|
10
|
+
export const POPOVER_VIEWPORT_MARGIN = 8
|
|
11
|
+
|
|
12
|
+
/** Écart par défaut entre le champ et le panneau */
|
|
13
|
+
export const POPOVER_GAP = 8
|
|
14
|
+
|
|
15
|
+
/** Espace vertical visé sous le champ — en dessous, on cherche la place au-dessus */
|
|
16
|
+
export const POPOVER_MIN_SPACE = 220
|
|
17
|
+
|
|
18
|
+
/** Largeur de repli tant que le panneau n'a pas été mesuré (1er rendu) */
|
|
19
|
+
export const POPOVER_FALLBACK_WIDTH = 360
|
|
20
|
+
|
|
21
|
+
/** Marge minimale de la flèche avec les coins du panneau */
|
|
22
|
+
export const POPOVER_CARET_INSET = 16
|
|
23
|
+
|
|
24
|
+
/** Rectangle du champ d'ancrage (coordonnées fenêtre) */
|
|
25
|
+
export interface PopoverAnchor {
|
|
26
|
+
top: number
|
|
27
|
+
bottom: number
|
|
28
|
+
left: number
|
|
29
|
+
width: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Fenêtre visible */
|
|
33
|
+
export interface PopoverViewport {
|
|
34
|
+
width: number
|
|
35
|
+
height: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface PlacePopoverOptions {
|
|
39
|
+
/** Rectangle du champ auquel le panneau s'ancre */
|
|
40
|
+
anchor: PopoverAnchor
|
|
41
|
+
viewport: PopoverViewport
|
|
42
|
+
/** Largeur réelle du panneau (mesurée après rendu) */
|
|
43
|
+
panelWidth?: number
|
|
44
|
+
/** Écart champ ↔ panneau (8px par défaut) */
|
|
45
|
+
gap?: number
|
|
46
|
+
/** Marge fenêtre (8px par défaut) */
|
|
47
|
+
margin?: number
|
|
48
|
+
/** Espace vertical minimal visé (220px par défaut) */
|
|
49
|
+
minSpace?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PopoverPlacement {
|
|
53
|
+
/** Côté d'ouverture : `down` sous le champ, `up` au-dessus */
|
|
54
|
+
direction: "down" | "up"
|
|
55
|
+
/** Bord haut en `position: fixed` — `null` quand le panneau s'ouvre au-dessus */
|
|
56
|
+
top: number | null
|
|
57
|
+
/** Bord bas en `position: fixed` — `null` quand le panneau s'ouvre en dessous */
|
|
58
|
+
bottom: number | null
|
|
59
|
+
/** Bord gauche, recadré dans la fenêtre */
|
|
60
|
+
left: number
|
|
61
|
+
/** Hauteur maximale du panneau (au-delà : scroll interne) */
|
|
62
|
+
maxHeight: number
|
|
63
|
+
/** Position de la flèche, depuis le bord gauche du panneau */
|
|
64
|
+
caret: number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
68
|
+
Math.min(Math.max(value, min), Math.max(min, max))
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Place un panneau ancré sous un champ et renvoie ses coordonnées `position: fixed`.
|
|
72
|
+
* Le côté (dessous / dessus), l'écart, la hauteur maximale et le recadrage horizontal
|
|
73
|
+
* dépendent de l'espace réellement visible dans la fenêtre.
|
|
74
|
+
*/
|
|
75
|
+
export function placePopover(options: PlacePopoverOptions): PopoverPlacement {
|
|
76
|
+
const { anchor, viewport, panelWidth = POPOVER_FALLBACK_WIDTH } = options
|
|
77
|
+
const gap0 = options.gap ?? POPOVER_GAP
|
|
78
|
+
const margin = options.margin ?? POPOVER_VIEWPORT_MARGIN
|
|
79
|
+
const minSpace = options.minSpace ?? POPOVER_MIN_SPACE
|
|
80
|
+
|
|
81
|
+
const below = viewport.height - anchor.bottom - margin
|
|
82
|
+
const above = anchor.top - margin
|
|
83
|
+
|
|
84
|
+
// Sous le champ dès qu'il y a la place ; sinon on prend le côté le plus large.
|
|
85
|
+
const down = below >= minSpace || below >= above
|
|
86
|
+
const available = Math.max(down ? below : above, 0)
|
|
87
|
+
|
|
88
|
+
// Écart réduit quand la place manque, pour laisser le maximum au panneau
|
|
89
|
+
const gap = clamp(gap0, 0, Math.max(available - minSpace, 0))
|
|
90
|
+
|
|
91
|
+
// Largeur : jamais plus large que la fenêtre (moins les deux marges)
|
|
92
|
+
const width = Math.min(panelWidth, Math.max(viewport.width - margin * 2, 0))
|
|
93
|
+
|
|
94
|
+
// Ancre horizontale : bord gauche du champ, recadré dans la fenêtre
|
|
95
|
+
const left = Math.round(clamp(anchor.left, margin, viewport.width - width - margin))
|
|
96
|
+
|
|
97
|
+
// Flèche : centrée sur le champ, maintenue à distance des coins
|
|
98
|
+
const caret = Math.round(
|
|
99
|
+
clamp(
|
|
100
|
+
anchor.left + anchor.width / 2 - left,
|
|
101
|
+
POPOVER_CARET_INSET,
|
|
102
|
+
width - POPOVER_CARET_INSET,
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
direction: down ? "down" : "up",
|
|
108
|
+
top: down ? Math.round(anchor.bottom + gap) : null,
|
|
109
|
+
bottom: down ? null : Math.round(viewport.height - anchor.top + gap),
|
|
110
|
+
left,
|
|
111
|
+
maxHeight: Math.round(Math.max(available - gap, 0)),
|
|
112
|
+
caret,
|
|
113
|
+
}
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dnax/ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"description": "UI design System",
|
|
6
6
|
"type": "module",
|
|
@@ -22,14 +22,6 @@
|
|
|
22
22
|
"vue": "^3.5.41"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@editorjs/checklist": "^1.6.0",
|
|
26
|
-
"@editorjs/code": "^2.9.4",
|
|
27
|
-
"@editorjs/delimiter": "^1.4.2",
|
|
28
|
-
"@editorjs/editorjs": "^2.31.6",
|
|
29
|
-
"@editorjs/header": "^2.8.9",
|
|
30
|
-
"@editorjs/list": "^2.0.9",
|
|
31
|
-
"@editorjs/paragraph": "^2.11.7",
|
|
32
|
-
"@editorjs/quote": "^2.7.6",
|
|
33
25
|
"@iconify/vue": "^5.0.1",
|
|
34
26
|
"@nuxt/kit": "4.5.2",
|
|
35
27
|
"@tiptap/extension-bubble-menu": "^3.31.3",
|
|
@@ -53,6 +45,7 @@
|
|
|
53
45
|
"arktype": "^2.2.3",
|
|
54
46
|
"class-variance-authority": "^0.7.1",
|
|
55
47
|
"clsx": "^2.1.1",
|
|
48
|
+
"echarts": "^6.1.0",
|
|
56
49
|
"embla-carousel": "^8.5.0",
|
|
57
50
|
"embla-carousel-vue": "^8.5.0",
|
|
58
51
|
"fuse.js": "^7.1.0",
|
|
@@ -56,6 +56,19 @@ export type { OverlayHandle } from "./lib/overlayBack"
|
|
|
56
56
|
export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
|
|
57
57
|
export { vRipple } from "./lib/ripple"
|
|
58
58
|
export type { RippleOptions, RippleValue } from "./lib/ripple"
|
|
59
|
+
export { chartToECharts, channelValue, pickFromEvent, tableModel, textValue } from "./lib/chart"
|
|
60
|
+
export type {
|
|
61
|
+
QChartAxis,
|
|
62
|
+
QChartChannel,
|
|
63
|
+
QChartColumn,
|
|
64
|
+
QChartConfig,
|
|
65
|
+
QChartLegend,
|
|
66
|
+
QChartMark,
|
|
67
|
+
QChartMarkType,
|
|
68
|
+
QChartOrientation,
|
|
69
|
+
QChartTable,
|
|
70
|
+
QChartPick,
|
|
71
|
+
} from "./lib/chart"
|
|
59
72
|
export { vTouchPan } from "./lib/touchPan"
|
|
60
73
|
export type { PanDetails, TouchPanHandler, PanDirection } from "./lib/touchPan"
|
|
61
74
|
export { vTouchHold } from "./lib/touchHold"
|