@dnax/ui 0.0.26 → 0.0.28

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,123 @@
1
+ <script setup lang="ts">
2
+ // QTiptapTableView — NodeView Vue du nœud « table » : rend un vrai <table>
3
+ // avec le contentDOM ProseMirror dans le <tbody>. Les rangées/cellules restent
4
+ // du contenu ProseMirror classique (sélection, merge/split, clavier).
5
+ // Ajouts maison : colgroup + poignées de redimensionnement de colonnes
6
+ // (drag), appliqué localement via des largeurs en % (non persistées dans le
7
+ // document — pour persister il faudrait écrire les attrs colwidth des cellules).
8
+ import { computed, onBeforeUnmount, ref, watch } from "vue"
9
+ import { nodeViewProps, NodeViewContent, NodeViewWrapper } from "@tiptap/vue-3"
10
+
11
+ const props = defineProps(nodeViewProps)
12
+
13
+ // Nombre de colonnes = largeur de la première rangée (les suivantes suivent
14
+ // les rowspans/colspans du schéma).
15
+ const cols = computed(() => {
16
+ const first = props.node?.firstChild
17
+ return first ? first.childCount : 0
18
+ })
19
+
20
+ // Largeurs en % (initialement égales) ; recalculees quand le nombre de
21
+ // colonnes change (ajout/suppression).
22
+ const widths = ref<number[]>([])
23
+ watch(
24
+ cols,
25
+ (count) => {
26
+ if (count === widths.value.length) return
27
+ const each = count > 0 ? 100 / count : 0
28
+ widths.value = Array.from({ length: count }, () => each)
29
+ },
30
+ { immediate: true },
31
+ )
32
+
33
+ // — Drag de redimensionnement (poignées entre colonnes) —
34
+ const tableEl = ref<HTMLElement | null>(null)
35
+ let dragging = false
36
+ let boundary = 0
37
+ let startX = 0
38
+ let leftStart = 0
39
+ let rightStart = 0
40
+ let tableWidth = 0
41
+
42
+ const onMove = (event: PointerEvent) => {
43
+ if (!dragging) return
44
+ const delta = event.clientX - startX
45
+ const minPx = 48
46
+ let leftPx = leftStart + delta
47
+ let rightPx = rightStart - delta
48
+ if (leftPx < minPx) {
49
+ rightPx -= minPx - leftPx
50
+ leftPx = minPx
51
+ }
52
+ if (rightPx < minPx) {
53
+ leftPx -= minPx - rightPx
54
+ rightPx = minPx
55
+ }
56
+ if (leftPx < minPx || rightPx < minPx) return
57
+ const total = tableWidth || 1
58
+ const next = [...widths.value]
59
+ next[boundary - 1] = (leftPx / total) * 100
60
+ next[boundary] = (rightPx / total) * 100
61
+ widths.value = next
62
+ }
63
+
64
+ const onUp = () => {
65
+ dragging = false
66
+ window.removeEventListener("pointermove", onMove)
67
+ window.removeEventListener("pointerup", onUp)
68
+ }
69
+
70
+ const startResize = (event: PointerEvent, boundaryIndex: number) => {
71
+ if (!props.editor.isEditable) return
72
+ event.preventDefault()
73
+ const table = tableEl.value
74
+ if (!table) return
75
+ const row = table.querySelector("tr")
76
+ if (!row) return
77
+ const leftCell = row.children[boundaryIndex - 1] as HTMLElement | undefined
78
+ const rightCell = row.children[boundaryIndex] as HTMLElement | undefined
79
+ if (!leftCell || !rightCell) return
80
+ tableWidth = table.getBoundingClientRect().width || 1
81
+ leftStart = leftCell.getBoundingClientRect().width
82
+ rightStart = rightCell.getBoundingClientRect().width
83
+ startX = event.clientX
84
+ boundary = boundaryIndex
85
+ dragging = true
86
+ window.addEventListener("pointermove", onMove)
87
+ window.addEventListener("pointerup", onUp)
88
+ }
89
+
90
+ onBeforeUnmount(() => {
91
+ window.removeEventListener("pointermove", onMove)
92
+ window.removeEventListener("pointerup", onUp)
93
+ })
94
+
95
+ // Position d'une poignée (bord droit de la colonne index) en %
96
+ const boundaryLeft = (index: number) => {
97
+ const acc = widths.value.slice(0, index).reduce((sum, w) => sum + w, 0)
98
+ return Math.min(acc, 99.5)
99
+ }
100
+ </script>
101
+
102
+ <template>
103
+ <node-view-wrapper as="div" class="q-tiptap__table-wrapper">
104
+ <table ref="tableEl" class="q-tiptap__table" :class="{ 'q-tiptap__table--resizable': editor.isEditable }">
105
+ <colgroup v-if="widths.length">
106
+ <col v-for="(width, i) in widths" :key="i" :style="{ width: width + '%' }" />
107
+ </colgroup>
108
+ <node-view-content as="tbody" />
109
+ </table>
110
+
111
+ <!-- Poignées de redimensionnement entre les colonnes (éditable seulement) -->
112
+ <span
113
+ v-for="index in Math.max(cols - 1, 0)"
114
+ :key="'handle-' + index"
115
+ class="q-tiptap__resize-handle"
116
+ :style="{ left: boundaryLeft(index) + '%' }"
117
+ role="separator"
118
+ aria-orientation="vertical"
119
+ :aria-label="'Resize column ' + index"
120
+ @pointerdown.prevent.stop="startResize($event, index)"
121
+ />
122
+ </node-view-wrapper>
123
+ </template>
@@ -0,0 +1,49 @@
1
+ <script setup lang="ts">
2
+ // QTiptapTaskItemView — NodeView Vue du nœud « taskItem » : rend une vraie
3
+ // <q-checkbox> dnax.ui (au lieu du <input> natif du DOM par défaut).
4
+ // Le nœud est fourni par l'extension officielle TaskItem (schéma, keymap
5
+ // Enter/Tab, input rules "[ ]" / "[x]") dont on ne remplace que addNodeView.
6
+ //
7
+ // ⚠ NodeView Vue : la RACINE DOIT être <node-view-wrapper> (erreur Tiptap
8
+ // « Please use the NodeViewWrapper component » sinon). En v3 ce wrapper ne
9
+ // propage pas les attrs → data-checked/class cochée portés par le div interne
10
+ // qui contient <node-view-content> (le contentDOM peut être imbriqué).
11
+ import { nodeViewProps, NodeViewContent, NodeViewWrapper } from "@tiptap/vue-3"
12
+ import QCheckbox from "../QCheckbox.vue"
13
+
14
+ // nodeViewProps : descripteurs runtime officiels (editor, node, updateAttributes,
15
+ // getPos…) — pattern documenté pour les NodeViews Vue dans Tiptap.
16
+ const props = defineProps(nodeViewProps)
17
+
18
+ const checked = () => !!props.node.attrs.checked
19
+ const isEditable = () => props.editor.isEditable
20
+
21
+ const onToggle = (value: unknown) => {
22
+ if (!isEditable()) return
23
+ if (typeof props.getPos !== "function") return
24
+ props.editor.chain().focus().command(({ tr }) => {
25
+ const position = props.getPos()
26
+ if (typeof position !== "number") return false
27
+ const current = tr.doc.nodeAt(position)
28
+ tr.setNodeMarkup(position, undefined, { ...current?.attrs, checked: Boolean(value) })
29
+ return true
30
+ }).run()
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <node-view-wrapper as="li">
36
+ <label class="q-tiptap__task-check" contenteditable="false" @mousedown.prevent>
37
+ <q-checkbox
38
+ :model-value="checked()"
39
+ dense
40
+ :disable="!isEditable()"
41
+ aria-label="Task done"
42
+ @update:model-value="onToggle"
43
+ />
44
+ </label>
45
+ <div class="q-tiptap__task-content" :class="{ 'q-tiptap__task-content--checked': checked() }">
46
+ <node-view-content />
47
+ </div>
48
+ </node-view-wrapper>
49
+ </template>
package/index.ts CHANGED
@@ -44,6 +44,7 @@ export { default as QDialogFooter } from "./components/QDialogFooter.vue"
44
44
  export { default as QDialogHeader } from "./components/QDialogHeader.vue"
45
45
  export { default as QDialogProvider } from "./components/QDialogProvider.vue"
46
46
  export { default as QDialogTrigger } from "./components/QDialogTrigger.vue"
47
+ export { default as QEditorJs } from "./components/QEditorJs.vue"
47
48
  export { default as QFab } from "./components/QFab.vue"
48
49
  export { default as QFabAction } from "./components/QFabAction.vue"
49
50
  export { default as QFilePicker } from "./components/QFilePicker.vue"
@@ -122,6 +123,7 @@ export { default as QTable } from "./components/QTable.vue"
122
123
  export { default as QTabs } from "./components/QTabs.vue"
123
124
  export { default as QText } from "./components/QText.vue"
124
125
  export { default as QTextCaption } from "./components/QTextCaption.vue"
126
+ export { default as QTiptap } from "./components/QTiptap.vue"
125
127
  export { default as QToolbar } from "./components/QToolbar.vue"
126
128
  export { default as QTooltip } from "./components/QTooltip.vue"
127
129
  export { default as QUploader } from "./components/QUploader.vue"
@@ -144,6 +146,8 @@ export { screen } from "./lib/screen"
144
146
  export type { QScreen } from "./lib/screen"
145
147
  export { loading } from "./lib/loading"
146
148
  export type { LoadingOptions, QLoadingPlugin } from "./lib/loading"
149
+ export { localStorage, sessionStorage } from "./lib/storage"
150
+ export type { QWebStorage } from "./lib/storage"
147
151
  export {
148
152
  registerOverlay,
149
153
  unregisterOverlay,
@@ -154,3 +158,11 @@ export {
154
158
  } from "./lib/overlayBack"
155
159
  export type { OverlayHandle } from "./lib/overlayBack"
156
160
  export { markOverlayClose, closeParentOverlay, vClose } from "./lib/closeOverlay"
161
+ export { vTouchPan } from "./lib/touchPan"
162
+ export type { PanDetails, TouchPanHandler, PanDirection } from "./lib/touchPan"
163
+ export { vTouchHold } from "./lib/touchHold"
164
+ export type { TouchHoldDetails, TouchHoldHandler } from "./lib/touchHold"
165
+ export { vTouchSwipe } from "./lib/touchSwipe"
166
+ export type { TouchSwipeDetails, TouchSwipeHandler, SwipeDirection } from "./lib/touchSwipe"
167
+ export { vTouchRepeat } from "./lib/touchRepeat"
168
+ export type { TouchRepeatDetails, TouchRepeatHandler } from "./lib/touchRepeat"
package/lib/icons.ts CHANGED
@@ -35,4 +35,30 @@ export const icons = {
35
35
  search: "lucide:search",
36
36
  star: "lucide:star",
37
37
  x: "lucide:x",
38
+ // — éditeur de texte (QTiptap) —
39
+ undo2: "lucide:undo-2",
40
+ redo2: "lucide:redo-2",
41
+ pilcrow: "lucide:pilcrow",
42
+ heading1: "lucide:heading-1",
43
+ heading2: "lucide:heading-2",
44
+ heading3: "lucide:heading-3",
45
+ bold: "lucide:bold",
46
+ italic: "lucide:italic",
47
+ underline: "lucide:underline",
48
+ strikethrough: "lucide:strikethrough",
49
+ code: "lucide:code",
50
+ link: "lucide:link",
51
+ unlink: "lucide:unlink",
52
+ list: "lucide:list",
53
+ listOrdered: "lucide:list-ordered",
54
+ quote: "lucide:quote",
55
+ codeXml: "lucide:code-xml",
56
+ removeFormatting: "lucide:remove-formatting",
57
+ palette: "lucide:palette",
58
+ eraser: "lucide:eraser",
59
+ alignLeft: "lucide:align-left",
60
+ alignCenter: "lucide:align-center",
61
+ alignRight: "lucide:align-right",
62
+ listChecks: "lucide:list-checks",
63
+ table: "lucide:table",
38
64
  } as const
package/lib/q.ts CHANGED
@@ -14,6 +14,7 @@ import QNotifyToast from "../components/QNotifyToast.vue"
14
14
  import { platform, qBreakpoints } from "./platform"
15
15
  import { screen } from "./screen"
16
16
  import { loading } from "./loading"
17
+ import { localStorage, sessionStorage } from "./storage"
17
18
 
18
19
  export interface DialogOptions {
19
20
  /** Composant de dialogue (SFC importé) ou nom de composant global */
@@ -323,8 +324,9 @@ function notify(opts: NotifyOptions): NotifyController {
323
324
  }
324
325
 
325
326
  // API publique : $q.dialog / $q.bottomSheet / $q.notify / $q.imagePreview / $q.platform / $q.breakpoints / $q.screen / $q.loading
327
+ // / $q.localStorage / $q.sessionStorage (stockage web typé)
326
328
  // Conventions Quasar : .open() pour les piles (dialog, bottomSheet, imagePreview),
327
- // .show() pour notify et loading.
329
+ // .show() pour notify et loading ; méthodes directes pour le stockage.
328
330
  const notifyPlugin = notify as typeof notify & { show: typeof notify }
329
331
  notifyPlugin.show = notify
330
332
 
@@ -343,6 +345,8 @@ export const $q = {
343
345
  breakpoints: qBreakpoints,
344
346
  screen,
345
347
  loading,
348
+ localStorage,
349
+ sessionStorage,
346
350
  }
347
351
 
348
352
  /** Accès programmatique aux plugins ($q.dialog, $q.notify, $q.loading…) */
package/lib/storage.ts ADDED
@@ -0,0 +1,175 @@
1
+ // $q.localStorage / $q.sessionStorage — stockage web façon Quasar
2
+ // (https://quasar.dev/quasar-plugins/web-storage).
3
+ //
4
+ // Les valeurs sont sérialisées avec leur type d'origine (Date, RegExp, number,
5
+ // boolean, string, objets/tableaux) grâce à un encodage préfixé — le format
6
+ // `__q_*` est volontairement IDENTIQUE à celui de Quasar : des clés écrites par
7
+ // une app Quasar restent lisibles par dnax.ui et inversement.
8
+ //
9
+ // Autres types (undefined, null, bigint, symbol, fonction) : stockés sous forme
10
+ // de string (comportement Quasar).
11
+ //
12
+ // SSR-safe : côté serveur (ou quand le Web Storage est indisponible — mode
13
+ // privé, iframe sandbox…), toutes les méthodes deviennent des no-op et
14
+ // getItem/getIndex/getKey renvoient null, getAll {}, getAllKeys [].
15
+
16
+ export interface QWebStorage {
17
+ /** true si la clé existe */
18
+ has(key: string): boolean
19
+ /** Alias de has() */
20
+ hasItem(key: string): boolean
21
+ /** Nombre de clés stockées */
22
+ getLength(): number
23
+ /** Valeur (typée) de la clé, ou null si absente */
24
+ getItem<T = unknown>(key: string): T | null
25
+ /** Valeur (typée) stockée à l'index donné (ordre du Web Storage), ou null */
26
+ getIndex<T = unknown>(index: number): T | null
27
+ /** Clé stockée à l'index donné, ou null */
28
+ getKey(index: number): string | null
29
+ /** Toutes les entrées : { clé: valeur typée } */
30
+ getAll(): Record<string, unknown>
31
+ /** Toutes les clés, dans l'ordre du Web Storage */
32
+ getAllKeys(): string[]
33
+ /** Stocke une valeur (sérialisation typée) — alias de setItem() */
34
+ set(key: string, value: unknown): void
35
+ /** Stocke une valeur (sérialisation typée) */
36
+ setItem(key: string, value: unknown): void
37
+ /** Supprime la clé — alias de removeItem() */
38
+ remove(key: string): void
39
+ /** Supprime la clé */
40
+ removeItem(key: string): void
41
+ /** Vide tout le stockage */
42
+ clear(): void
43
+ /** true si le stockage est vide */
44
+ isEmpty(): boolean
45
+ }
46
+
47
+ // — Sérialisation typée (format compatible Quasar) —
48
+ // Encode : `__q_<type>|<payload>` — tout type supporté ré-encode ses données
49
+ // pour que le Web Storage (qui ne stocke que des strings) restitue le type.
50
+
51
+ function encode(value: unknown): string {
52
+ if (value instanceof Date) return `__q_date|${value.getTime()}`
53
+ if (value instanceof RegExp) return `__q_expr|${value.source}`
54
+ switch (typeof value) {
55
+ case "number": return `__q_numb|${value}`
56
+ case "boolean": return `__q_bool|${value ? "1" : "0"}`
57
+ case "string": return `__q_strn|${value}`
58
+ case "function": return `__q_strn|${value.toString()}`
59
+ case "object":
60
+ if (value !== null) return `__q_objt|${JSON.stringify(value)}`
61
+ break
62
+ }
63
+ // Type non supporté (undefined, null, bigint, symbol…) : stocké tel quel
64
+ // (le Web Storage le convertit en string — même résultat côté lecture).
65
+ return String(value)
66
+ }
67
+
68
+ const NUMBER_RE = /^-?\d+$/
69
+
70
+ function decode(value: string): unknown {
71
+ // Valeur non encodée par nous (< préfixe, ou préfixe inconnu) : telle quelle
72
+ if (value.length < 9) return value
73
+ const type = value.slice(0, 8)
74
+ const source = value.slice(9)
75
+
76
+ switch (type) {
77
+ case "__q_date": {
78
+ // Horodatage numérique (format actuel) ou string ISO (anciens formats)
79
+ return new Date(NUMBER_RE.test(source) ? Number.parseInt(source, 10) : source)
80
+ }
81
+ case "__q_expr":
82
+ return new RegExp(source)
83
+ case "__q_numb":
84
+ return Number(source)
85
+ case "__q_bool":
86
+ return source === "1"
87
+ case "__q_strn":
88
+ return source
89
+ case "__q_objt": {
90
+ try {
91
+ return JSON.parse(source)
92
+ }
93
+ catch {
94
+ // Payload corrompu → valeur brute plutôt que de faire planter getItem()
95
+ return value
96
+ }
97
+ }
98
+ default:
99
+ return value
100
+ }
101
+ }
102
+
103
+ // Instance no-op pour SSR / Web Storage indisponible (miroir de Quasar :
104
+ // getEmptyStorage) — jamais de throw ni d'accès à window.
105
+ function emptyStorage(): QWebStorage {
106
+ const noop = () => {}
107
+ return {
108
+ has: () => false,
109
+ hasItem: () => false,
110
+ getLength: () => 0,
111
+ getItem: () => null,
112
+ getIndex: () => null,
113
+ getKey: () => null,
114
+ getAll: () => ({}),
115
+ getAllKeys: () => [],
116
+ set: noop,
117
+ setItem: noop,
118
+ remove: noop,
119
+ removeItem: noop,
120
+ clear: noop,
121
+ isEmpty: () => true,
122
+ }
123
+ }
124
+
125
+ function init(type: "local" | "session"): QWebStorage {
126
+ try {
127
+ if (typeof window === "undefined") return emptyStorage()
128
+ const ws = type === "local" ? window.localStorage : window.sessionStorage
129
+ if (!ws) return emptyStorage()
130
+
131
+ const get = (key: string): unknown => {
132
+ const item = ws.getItem(key)
133
+ return item === null ? null : decode(item)
134
+ }
135
+
136
+ return {
137
+ has: (key) => ws.getItem(key) !== null,
138
+ hasItem: (key) => ws.getItem(key) !== null,
139
+ getLength: () => ws.length,
140
+ getItem: <T = unknown>(key: string): T | null => get(key) as T | null,
141
+ getIndex: <T = unknown>(index: number): T | null =>
142
+ index < ws.length ? (get(ws.key(index) as string) as T | null) : null,
143
+ getKey: (index) => (index < ws.length ? ws.key(index) : null),
144
+ getAll: () => {
145
+ const result: Record<string, unknown> = {}
146
+ for (let i = 0; i < ws.length; i++) {
147
+ const key = ws.key(i) as string
148
+ result[key] = get(key)
149
+ }
150
+ return result
151
+ },
152
+ getAllKeys: () => {
153
+ const result: string[] = []
154
+ for (let i = 0; i < ws.length; i++) result.push(ws.key(i) as string)
155
+ return result
156
+ },
157
+ set: (key, value) => ws.setItem(key, encode(value)),
158
+ setItem: (key, value) => ws.setItem(key, encode(value)),
159
+ remove: (key) => ws.removeItem(key),
160
+ removeItem: (key) => ws.removeItem(key),
161
+ clear: () => ws.clear(),
162
+ isEmpty: () => ws.length === 0,
163
+ }
164
+ }
165
+ catch {
166
+ // Accès bloqué (mode privé, cookies désactivés…) → instance no-op
167
+ return emptyStorage()
168
+ }
169
+ }
170
+
171
+ /** $q.localStorage — stockage persistant (API Quasar Web Storage) */
172
+ export const localStorage: QWebStorage = init("local")
173
+
174
+ /** $q.sessionStorage — stockage par session (API Quasar Web Storage) */
175
+ export const sessionStorage: QWebStorage = init("session")
@@ -0,0 +1,19 @@
1
+ // Tables Tiptap pour QTiptap — le nœud « table » est rendu par une NodeView
2
+ // Vue (QTiptapTableView) au lieu du rendu DOM/colgroup par défaut. On garde
3
+ // les nœuds officiels TableRow/TableHeader/TableCell (schéma, keymaps,
4
+ // commandes) ; on ne remplace que le addNodeView de « table ».
5
+ import { Table, TableHeader, TableCell, TableRow } from "@tiptap/extension-table"
6
+ import { VueNodeViewRenderer } from "@tiptap/vue-3"
7
+ import type { NodeViewProps } from "@tiptap/vue-3"
8
+ import type { Component } from "vue"
9
+ import QTiptapTableView from "../components/internal/QTiptapTableView.vue"
10
+
11
+ export { TableHeader, TableCell, TableRow }
12
+
13
+ export const QTipTapTable = Table.extend({
14
+ addNodeView() {
15
+ // defineProps(nodeViewProps) ne fait pas remonter les types vers le renderer
16
+ // → cast vers Component<NodeViewProps>
17
+ return VueNodeViewRenderer(QTiptapTableView as Component<NodeViewProps>)
18
+ },
19
+ })
@@ -0,0 +1,16 @@
1
+ // Task list Tiptap pour QTiptap — l'élément de liste est rendu par une
2
+ // NodeView Vue (q-checkbox dnax.ui) : on garde l'extension officielle
3
+ // TaskItem (schéma, parse/render, keymap, input rules) et on ne remplace que
4
+ // son addNodeView par VueNodeViewRenderer(QTiptapTaskItemView).
5
+ import TaskList from "@tiptap/extension-task-list"
6
+ import TaskItem from "@tiptap/extension-task-item"
7
+ import { VueNodeViewRenderer } from "@tiptap/vue-3"
8
+ import QTiptapTaskItemView from "../components/internal/QTiptapTaskItemView.vue"
9
+
10
+ export { TaskList }
11
+
12
+ export const QTipTapTaskItem = TaskItem.extend({
13
+ addNodeView() {
14
+ return VueNodeViewRenderer(QTiptapTaskItemView)
15
+ },
16
+ })