@dnax/ui 0.0.28 → 0.0.29

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.
@@ -0,0 +1,118 @@
1
+ // v-intersection — directive : appelle un handler quand l'élément entre /
2
+ // sort du viewport (Intersection Observer), équivalent Quasar
3
+ // https://quasar.dev/vue-directives/intersection
4
+ //
5
+ // Usage :
6
+ // <div v-intersection="onEnter">…</div>
7
+ // <div v-intersection.once="onFirstEnter">…</div>
8
+ // <div v-intersection="{ handler, cfg: { rootMargin: '0px', threshold: 0.2 } }">…</div>
9
+ // <div v-intersection="false">…</div> (désactive l'observation)
10
+ //
11
+ // Le handler reçoit un IntersectionObserverEntry. Les éléments partageant les
12
+ // mêmes options (cfg) utilisent le même IntersectionObserver sous le capot.
13
+
14
+ export type IntersectionValue =
15
+ | ((entry: IntersectionObserverEntry) => void)
16
+ | { handler: (entry: IntersectionObserverEntry) => void; cfg?: IntersectionObserverInit }
17
+ | false
18
+ | undefined
19
+ | null
20
+
21
+ type PoolEntry = {
22
+ key: string
23
+ observer: IntersectionObserver
24
+ elements: Set<Element>
25
+ }
26
+
27
+ type QElement = HTMLElement & {
28
+ __qIntersection?: {
29
+ key: string
30
+ observer: IntersectionObserver
31
+ handler: (e: IntersectionObserverEntry) => void
32
+ once: boolean
33
+ raw: IntersectionValue
34
+ }
35
+ }
36
+
37
+ const pool = new Map<string, PoolEntry>()
38
+
39
+ function getPoolEntry(cfg: IntersectionObserverInit): PoolEntry {
40
+ const key = JSON.stringify([
41
+ cfg.rootMargin ?? "0px",
42
+ cfg.threshold ?? 0,
43
+ cfg.root === null ? null : cfg.root instanceof Element ? (cfg.root as Element).id || "" : String(cfg.root),
44
+ ])
45
+ let entry = pool.get(key)
46
+ if (!entry) {
47
+ const elements = new Set<Element>()
48
+ const observer = new IntersectionObserver((entries) => {
49
+ for (const e of entries) {
50
+ const rec = (e.target as QElement).__qIntersection
51
+ if (!rec) continue
52
+ if (rec.once && e.isIntersecting) {
53
+ rec.handler(e)
54
+ unobserve(e.target as QElement)
55
+ continue
56
+ }
57
+ rec.handler(e)
58
+ }
59
+ }, cfg)
60
+ entry = { key, observer, elements }
61
+ pool.set(key, entry)
62
+ }
63
+ return entry
64
+ }
65
+
66
+ function unobserve(el: QElement) {
67
+ const rec = el.__qIntersection
68
+ if (!rec) return
69
+ const entry = pool.get(rec.key)
70
+ el.__qIntersection = undefined
71
+ rec.observer.unobserve(el)
72
+ if (entry) {
73
+ entry.elements.delete(el)
74
+ if (entry.elements.size === 0) {
75
+ entry.observer.disconnect()
76
+ pool.delete(rec.key)
77
+ }
78
+ }
79
+ }
80
+
81
+ function apply(el: HTMLElement, value: IntersectionValue, once = false) {
82
+ const target = el as QElement
83
+ if (target.__qIntersection) unobserve(target)
84
+ if (!value) return
85
+
86
+ let handler: (entry: IntersectionObserverEntry) => void
87
+ let cfg: IntersectionObserverInit = { root: null, rootMargin: "0px", threshold: 0 }
88
+
89
+ if (typeof value === "function") {
90
+ handler = value
91
+ }
92
+ else if (typeof value === "object") {
93
+ handler = value.handler
94
+ if (value.cfg) cfg = { ...value.cfg }
95
+ }
96
+ else {
97
+ return
98
+ }
99
+
100
+ const entry = getPoolEntry(cfg)
101
+ target.__qIntersection = { key: entry.key, observer: entry.observer, handler, once, raw: value }
102
+ entry.elements.add(el)
103
+ entry.observer.observe(el)
104
+ }
105
+
106
+ export const vIntersection = {
107
+ mounted(el: HTMLElement, binding: { value?: IntersectionValue; modifiers?: Record<string, boolean> }) {
108
+ apply(el, binding.value, !!binding.modifiers?.once)
109
+ },
110
+ updated(el: HTMLElement, binding: { value?: IntersectionValue; modifiers?: Record<string, boolean> }) {
111
+ const rec = (el as QElement).__qIntersection
112
+ if (rec && rec.raw === binding.value) return
113
+ apply(el, binding.value, !!binding.modifiers?.once)
114
+ },
115
+ unmounted(el: HTMLElement) {
116
+ unobserve(el as QElement)
117
+ },
118
+ }
@@ -0,0 +1,92 @@
1
+ // Tests unitaires des helpers purs du tableur (bun test).
2
+ import { describe, expect, it } from "bun:test"
3
+ import {
4
+ colLetter,
5
+ colFromLetters,
6
+ isBlankValue,
7
+ isoAddDays,
8
+ shiftFormulaRefs,
9
+ csvEscape,
10
+ csvSplitLine,
11
+ parseCsv,
12
+ deriveCols,
13
+ } from "./spreadsheet"
14
+
15
+ describe("lettres de colonnes", () => {
16
+ it("colLetter : 0→A, 25→Z, 26→AA, 27→AB, 701→ZZ, 702→AAA", () => {
17
+ expect(colLetter(0)).toBe("A")
18
+ expect(colLetter(25)).toBe("Z")
19
+ expect(colLetter(26)).toBe("AA")
20
+ expect(colLetter(27)).toBe("AB")
21
+ expect(colLetter(701)).toBe("ZZ")
22
+ expect(colLetter(702)).toBe("AAA")
23
+ })
24
+ it("colFromLetters est l'inverse", () => {
25
+ expect(colFromLetters("A")).toBe(0)
26
+ expect(colFromLetters("AA")).toBe(26)
27
+ expect(colFromLetters("zz")).toBe(701)
28
+ for (let i = 0; i < 200; i++) expect(colFromLetters(colLetter(i))).toBe(i)
29
+ })
30
+ })
31
+
32
+ describe("shiftFormulaRefs (autofill)", () => {
33
+ it("décale les références relatives de (dr,dc)", () => {
34
+ expect(shiftFormulaRefs("=B1*C1", 1, 0)).toBe("=B2*C2")
35
+ expect(shiftFormulaRefs("=A1", 0, 2)).toBe("=C1")
36
+ })
37
+ it("préserve les $ absolus", () => {
38
+ expect(shiftFormulaRefs("=$B$1+A1", 2, 0)).toBe("=$B$1+A3")
39
+ expect(shiftFormulaRefs("=$B1+B$1", 1, 1)).toBe("=$B2+C$1")
40
+ })
41
+ it("décale les plages entières", () => {
42
+ expect(shiftFormulaRefs("=SUM(D1:D4)", 1, 0)).toBe("=SUM(D2:D5)")
43
+ })
44
+ it("laisse les valeurs sans lettres intactes", () => {
45
+ expect(shiftFormulaRefs("=1+2", 1, 1)).toBe("=1+2")
46
+ })
47
+ })
48
+
49
+ describe("dates ISO", () => {
50
+ it("isoAddDays", () => {
51
+ expect(isoAddDays("2026-09-07", 1)).toBe("2026-09-08")
52
+ expect(isoAddDays("2026-09-07", -7)).toBe("2026-08-31")
53
+ expect(isoAddDays("2026-12-31", 1)).toBe("2027-01-01")
54
+ })
55
+ it("isBlankValue", () => {
56
+ expect(isBlankValue(null)).toBe(true)
57
+ expect(isBlankValue(undefined)).toBe(true)
58
+ expect(isBlankValue("")).toBe(true)
59
+ expect(isBlankValue(0)).toBe(false)
60
+ expect(isBlankValue("0")).toBe(false)
61
+ })
62
+ })
63
+
64
+ describe("CSV", () => {
65
+ it("csvEscape quote si nécessaire", () => {
66
+ expect(csvEscape("simple", ",")).toBe("simple")
67
+ expect(csvEscape("a,b", ",")).toBe('"a,b"')
68
+ expect(csvEscape('say "hi"', ",")).toBe('"say ""hi"""')
69
+ expect(csvEscape("ligne\nsuivante", ";")).toBe('"ligne\nsuivante"')
70
+ expect(csvEscape(null, ",")).toBe("")
71
+ })
72
+ it("csvSplitLine gère les guillemets doublés et les délimiteurs", () => {
73
+ expect(csvSplitLine('a,"b,c",d', ",")).toEqual(["a", "b,c", "d"])
74
+ expect(csvSplitLine('"x""y",z', ",")).toEqual(['x"y', "z"])
75
+ expect(csvSplitLine("a;b", ";")).toEqual(["a", "b"])
76
+ })
77
+ it("parseCsv : multiligne, quotes, CRLF", () => {
78
+ const text = 'h1,h2\r\n"a,1","b""2"\r\nc,d\n'
79
+ expect(parseCsv(text)).toEqual([
80
+ ["h1", "h2"],
81
+ ["a,1", 'b"2'],
82
+ ["c", "d"],
83
+ ])
84
+ })
85
+ it("deriveCols déduit les colonnes des clés", () => {
86
+ expect(deriveCols([{ a: 1, b: "x" }, { b: 2, c: true }])).toEqual([
87
+ { name: "a", type: "text" },
88
+ { name: "b", type: "text" },
89
+ { name: "c", type: "text" },
90
+ ])
91
+ })
92
+ })
@@ -0,0 +1,175 @@
1
+ // Helpers purs du tableur QSpreadsheet — extraits du composant pour être
2
+ // testables unitairement (bun test). Aucune dépendance DOM / Vue.
3
+
4
+ /** Index de colonne (0-based) → lettres Excel : 0 → "A", 25 → "Z", 26 → "AA" */
5
+ export const colLetter = (i: number): string => {
6
+ let n = i
7
+ let s = ""
8
+ while (n >= 0) {
9
+ s = String.fromCharCode(65 + (n % 26)) + s
10
+ n = Math.floor(n / 26) - 1
11
+ }
12
+ return s
13
+ }
14
+
15
+ /** Lettres Excel → index de colonne 0-based : "A" → 0, "AA" → 26 */
16
+ export const colFromLetters = (s: string): number => {
17
+ let n = 0
18
+ for (const ch of s.toUpperCase()) n = n * 26 + (ch.charCodeAt(0) - 64)
19
+ return n - 1
20
+ }
21
+
22
+ /** Cellule « vide » : null, undefined ou chaîne vide */
23
+ export const isBlankValue = (v: any): boolean =>
24
+ v === null || v === undefined || v === ""
25
+
26
+ /**
27
+ * Décale les références A1 non absolues d'une formule de (dr, dc) —
28
+ * plages incluses, `$` préservés (copie / autofill).
29
+ */
30
+ export const shiftFormulaRefs = (src: string, dr: number, dc: number): string =>
31
+ src.replace(
32
+ /(\$?)([A-Za-z]{1,3})(\$?)(\d+)/g,
33
+ (_m: string, ca: string, L: string, ra: string, D: string) => {
34
+ const col = colFromLetters(L) + (ca === "$" ? 0 : dc)
35
+ const row = Number(D) - 1 + (ra === "$" ? 0 : dr)
36
+ if (col < 0 || row < 0) return _m
37
+ return ca + colLetter(col) + ra + (row + 1)
38
+ },
39
+ )
40
+
41
+ /** Date ISO AAAA-MM-JJ */
42
+ export const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/
43
+
44
+ /** Ajoute des jours à une date ISO (UTC, évite le décalage de fuseau) */
45
+ export const isoAddDays = (iso: string, days: number): string => {
46
+ const d = new Date(iso + "T00:00:00Z")
47
+ d.setUTCDate(d.getUTCDate() + days)
48
+ return d.toISOString().slice(0, 10)
49
+ }
50
+
51
+ /** Échappe une valeur pour un CSV (quote si délimiteur / guillemet / newline) */
52
+ export const csvEscape = (s: unknown, delim: string): string => {
53
+ const str = s === null || s === undefined ? "" : String(s)
54
+ return str.includes(delim) || str.includes('"') || /[\n\r]/.test(str)
55
+ ? '"' + str.replace(/"/g, '""') + '"'
56
+ : str
57
+ }
58
+
59
+ /**
60
+ * Décode une ligne CSV (guillemets doublés, délimiteur multi-caractère géré
61
+ * comme un caractère unique ; retourne les cellules décodées).
62
+ */
63
+ export const csvSplitLine = (line: string, delim = ","): string[] => {
64
+ const out: string[] = []
65
+ let cur = ""
66
+ let inQ = false
67
+ let i = 0
68
+ const isDelim = (at: number) => line.startsWith(delim, at)
69
+ while (i < line.length) {
70
+ const ch = line[i]!
71
+ if (inQ) {
72
+ if (ch === '"') {
73
+ if (line[i + 1] === '"') {
74
+ cur += '"'
75
+ i += 2
76
+ continue
77
+ }
78
+ inQ = false
79
+ i++
80
+ continue
81
+ }
82
+ cur += ch
83
+ i++
84
+ continue
85
+ }
86
+ if (ch === '"') {
87
+ inQ = true
88
+ i++
89
+ continue
90
+ }
91
+ if (isDelim(i)) {
92
+ out.push(cur)
93
+ cur = ""
94
+ i += delim.length
95
+ continue
96
+ }
97
+ cur += ch
98
+ i++
99
+ }
100
+ out.push(cur)
101
+ return out
102
+ }
103
+
104
+ /**
105
+ * Parse un texte CSV complet (CRLF ou LF, guillemets) en lignes de cellules.
106
+ */
107
+ export const parseCsv = (text: string, delim = ","): string[][] => {
108
+ const lines: string[][] = []
109
+ const current: string[] = []
110
+ let cell = ""
111
+ let inQ = false
112
+ let sawDelim = false
113
+ let i = 0
114
+ const isDelim = (at: number) => text.startsWith(delim, at)
115
+ const pushCell = () => {
116
+ current.push(cell)
117
+ cell = ""
118
+ }
119
+ while (i < text.length) {
120
+ const ch = text[i]!
121
+ if (inQ) {
122
+ if (ch === '"') {
123
+ if (text[i + 1] === '"') {
124
+ cell += '"'
125
+ i += 2
126
+ continue
127
+ }
128
+ inQ = false
129
+ i++
130
+ continue
131
+ }
132
+ cell += ch
133
+ i++
134
+ continue
135
+ }
136
+ if (ch === '"') {
137
+ inQ = true
138
+ i++
139
+ continue
140
+ }
141
+ if (isDelim(i)) {
142
+ pushCell()
143
+ sawDelim = true
144
+ i += delim.length
145
+ continue
146
+ }
147
+ if (ch === "\n") {
148
+ pushCell()
149
+ lines.push(current.splice(0))
150
+ sawDelim = false
151
+ i++
152
+ continue
153
+ }
154
+ if (ch === "\r") {
155
+ // les retours \r\n sont déjà gérés par le \n
156
+ i++
157
+ continue
158
+ }
159
+ cell += ch
160
+ i++
161
+ }
162
+ pushCell()
163
+ const onlyTrailingNewline = current.length === 1 && current[0] === "" && !sawDelim
164
+ if (lines.length === 0 || !onlyTrailingNewline) lines.push(current)
165
+ return lines
166
+ }
167
+
168
+ /** Colonnes déduites des clés d'un jeu de lignes (type texte) */
169
+ export const deriveCols = (
170
+ rows: Record<string, any>[],
171
+ ): { name: string; type: "text" }[] => {
172
+ const keys = new Set<string>()
173
+ for (const r of rows) Object.keys(r).forEach((k) => keys.add(k))
174
+ return [...keys].map((k) => ({ name: k, type: "text" as const }))
175
+ }
@@ -1,6 +1,7 @@
1
1
  import { computed, inject, toValue } from "vue"
2
2
  import type { ComputedRef, MaybeRefOrGetter } from "vue"
3
3
  import { qConfigKey } from "./config"
4
+ import type { QAppLang } from "./config"
4
5
 
5
6
  /** Échelle de rayon : none (carré) → lg (très arrondi) */
6
7
  export type RadiusScale = "none" | "xs" | "sm" | "md" | "lg"
@@ -19,6 +20,12 @@ export const RADIUS_VALUES: Record<RadiusScale, string> = {
19
20
  export const isRadiusScale = (v: unknown): v is RadiusScale =>
20
21
  v === "none" || v === "xs" || v === "sm" || v === "md" || v === "lg"
21
22
 
23
+ /** Langue d'application fournie par QConfigProvider ("en" par défaut). */
24
+ export function useConfigLang(): ComputedRef<QAppLang> {
25
+ const config = inject(qConfigKey, null)
26
+ return computed(() => config?.lang.value ?? "en")
27
+ }
28
+
22
29
  /**
23
30
  * Props par défaut d'un composant depuis `theme.componentProps` :
24
31
  * `default` (appliqué à tous) puis `<Nom>` (spécifique, prioritaire).
package/module.ts CHANGED
@@ -97,6 +97,7 @@ import { vTouchPan } from "@dnax/ui/runtime"
97
97
  import { vTouchHold } from "@dnax/ui/runtime"
98
98
  import { vTouchSwipe } from "@dnax/ui/runtime"
99
99
  import { vTouchRepeat } from "@dnax/ui/runtime"
100
+ import { vIntersection } from "@dnax/ui/runtime"
100
101
 
101
102
  export default defineNuxtPlugin((nuxtApp) => {
102
103
  nuxtApp.vueApp.directive("close", vClose)
@@ -104,6 +105,7 @@ export default defineNuxtPlugin((nuxtApp) => {
104
105
  nuxtApp.vueApp.directive("touch-hold", vTouchHold)
105
106
  nuxtApp.vueApp.directive("touch-swipe", vTouchSwipe)
106
107
  nuxtApp.vueApp.directive("touch-repeat", vTouchRepeat)
108
+ nuxtApp.vueApp.directive("intersection", vIntersection)
107
109
  })
108
110
  `,
109
111
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dnax/ui",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "module": "index.ts",
5
5
  "description": "UI design System",
6
6
  "type": "module",
@@ -50,11 +50,13 @@
50
50
  "@tiptap/vue-3": "^3.31.3",
51
51
  "@videojs/html": "^10.0.0-beta.32",
52
52
  "@vueuse/core": "^14.4.0",
53
+ "arktype": "^2.2.3",
53
54
  "class-variance-authority": "^0.7.1",
54
55
  "clsx": "^2.1.1",
55
56
  "embla-carousel": "^8.5.0",
56
57
  "embla-carousel-vue": "^8.5.0",
57
58
  "fuse.js": "^7.1.0",
59
+ "moment": "^2.30.1",
58
60
  "shiki": "^3.0.0",
59
61
  "swiper": "^14.2.0",
60
62
  "tailwind-merge": "^3.6.0",
@@ -62,6 +62,8 @@ export { vTouchSwipe } from "./lib/touchSwipe"
62
62
  export type { TouchSwipeDetails, TouchSwipeHandler, SwipeDirection } from "./lib/touchSwipe"
63
63
  export { vTouchRepeat } from "./lib/touchRepeat"
64
64
  export type { TouchRepeatDetails, TouchRepeatHandler } from "./lib/touchRepeat"
65
+ export { vIntersection } from "./lib/intersection"
66
+ export type { IntersectionValue } from "./lib/intersection"
65
67
  `
66
68
 
67
69
  await Bun.write(indexFile, `${exports}\n${manualExports}`)