@dnax/ui 0.0.27 → 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.
@@ -41,10 +41,18 @@ interface Props {
41
41
  contentStyle?: StyleValue
42
42
  noBackdropDismiss?: boolean
43
43
  noEscDismiss?: boolean
44
- /** Animation : fade | zoom | slide-up | slide-down | slide-left | slide-right | swipe-left | swipe-right (sinon basée sur position) */
45
- transition?: "fade" | "zoom" | "slide-up" | "slide-down" | "slide-left" | "slide-right" | "swipe-left" | "swipe-right"
44
+ /** Animation : fade | zoom | slide-up | slide-down | sheet-up | sheet-down
45
+ * (même glissement que slide, ouverture qui décélère en fin de course) |
46
+ * slide-left | slide-right | swipe-left | swipe-right (sinon basée sur position) */
47
+ transition?: "fade" | "zoom" | "slide-up" | "slide-down" | "sheet-up" | "sheet-down" | "slide-left" | "slide-right" | "swipe-left" | "swipe-right"
46
48
  /** Durée des transitions d'entrée/sortie en ms (défaut CSS : ~200ms entrée, ~150ms sortie) */
47
49
  transitionDuration?: number
50
+ /** Courbe d'easing de l'OUVERTURE — valeur CSS ("cubic-bezier(0.32, 0.72, 0, 1)",
51
+ * "ease-out", "linear"…) — défaut : courbe propre à chaque transition
52
+ * (sheet-* = décélération en fin de course, slide-* = ease…) */
53
+ transitionEasingEnter?: string
54
+ /** Courbe d'easing de la FERMETURE — même principe que transition-easing-enter */
55
+ transitionEasingLeave?: string
48
56
  }
49
57
 
50
58
  const props = withDefaults(defineProps<Props>(), {
@@ -60,6 +68,8 @@ const props = withDefaults(defineProps<Props>(), {
60
68
  noBackdropDismiss: false,
61
69
  noEscDismiss: false,
62
70
  transitionDuration: undefined,
71
+ transitionEasingEnter: "",
72
+ transitionEasingLeave: "",
63
73
  })
64
74
 
65
75
  const emit = defineEmits<{
@@ -159,15 +169,19 @@ const transitionName = computed(() =>
159
169
  props.transition ? `q-dialog-${props.transition}` : "q-dialog",
160
170
  )
161
171
 
162
- // Durée des transitions (enter/leave) via variables CSS sur l'overlay
163
- const durationStyle = computed<StyleValue>(() =>
164
- props.transitionDuration
165
- ? {
166
- "--q-dialog-duration-enter": `${props.transitionDuration}ms`,
167
- "--q-dialog-duration-leave": `${props.transitionDuration}ms`,
168
- }
169
- : {},
170
- )
172
+ // Réglages des transitions (durée + courbes d'easing) exposés en variables CSS
173
+ // sur l'overlay : --q-dialog-duration-enter/leave et
174
+ // --q-dialog-easing-enter/leave (consommées par styles/main.css).
175
+ const transitionStyle = computed<StyleValue>(() => {
176
+ const style: Record<string, string> = {}
177
+ if (props.transitionDuration) {
178
+ style["--q-dialog-duration-enter"] = `${props.transitionDuration}ms`
179
+ style["--q-dialog-duration-leave"] = `${props.transitionDuration}ms`
180
+ }
181
+ if (props.transitionEasingEnter) style["--q-dialog-easing-enter"] = props.transitionEasingEnter
182
+ if (props.transitionEasingLeave) style["--q-dialog-easing-leave"] = props.transitionEasingLeave
183
+ return style
184
+ })
171
185
  </script>
172
186
 
173
187
  <template>
@@ -178,7 +192,7 @@ const durationStyle = computed<StyleValue>(() =>
178
192
  :ref="markOverlay"
179
193
  class="q-dialog__overlay"
180
194
  :class="overlayClasses"
181
- :style="durationStyle"
195
+ :style="transitionStyle"
182
196
  @mousedown.self="!persistent && !noBackdropDismiss && (open = false)"
183
197
  >
184
198
  <div
@@ -0,0 +1,146 @@
1
+ <script setup lang="ts">
2
+ // QEditorJs — éditeur par blocs basé sur Editor.js (https://editorjs.io).
3
+ // v-model = données JSON : { time?, blocks: [{ type, data }], version? }.
4
+ // Tools par défaut : paragraph, header (H1-H3), list (bullet/ordered),
5
+ // checklist, quote, code, delimiter. Extensible via la prop `tools`.
6
+ // SSR-safe : l'instance Editor.js n'est créée qu'au montage (client).
7
+ // Note : Editor.js v2 injecte lui-même ses styles à l'init (aucun import CSS
8
+ // nécessaire) ; les surcharges de thème vivent dans styles/main.css.
9
+ import { onBeforeUnmount, onMounted, ref, watch } from "vue"
10
+
11
+ interface QEditorJsBlock {
12
+ type: string
13
+ data: Record<string, unknown>
14
+ }
15
+
16
+ interface QEditorJsData {
17
+ time?: number
18
+ blocks: QEditorJsBlock[]
19
+ version?: string
20
+ }
21
+
22
+ interface Props {
23
+ /** Données du document (v-model) — { blocks: [{ type, data }] } */
24
+ data?: QEditorJsData | null
25
+ /** Placeholder du bloc par défaut */
26
+ placeholder?: string
27
+ /** Hauteur minimale de la zone d'édition (défaut : 220px) */
28
+ minHeight?: string
29
+ /** Lecture seule */
30
+ readonly?: boolean
31
+ /** Désactivé (lecture seule + style grisé) */
32
+ disable?: boolean
33
+ /** Tools supplémentaires (défaut : paragraph, header, list, checklist, quote, code, delimiter) */
34
+ tools?: Record<string, any>
35
+ }
36
+
37
+ const props = withDefaults(defineProps<Props>(), {
38
+ data: null,
39
+ placeholder: "Write something…",
40
+ minHeight: "220px",
41
+ readonly: false,
42
+ disable: false,
43
+ tools: () => ({}),
44
+ })
45
+
46
+ const emit = defineEmits<{
47
+ "update:modelValue": [data: QEditorJsData]
48
+ ready: [editor: unknown]
49
+ }>()
50
+
51
+ const holderEl = ref<HTMLElement | null>(null)
52
+ let editor: any = null
53
+ let lastJson = ""
54
+ let internal = false
55
+
56
+ const isEditable = () => !props.readonly && !props.disable
57
+
58
+ const emitData = async () => {
59
+ if (!editor) return
60
+ try {
61
+ const saved: QEditorJsData = (await editor.save()) as QEditorJsData
62
+ internal = true
63
+ lastJson = JSON.stringify(saved)
64
+ emit("update:modelValue", saved)
65
+ internal = false
66
+ }
67
+ catch {
68
+ /* document invalide — ignoré */
69
+ }
70
+ }
71
+
72
+ onMounted(async () => {
73
+ const mod = await import("@editorjs/editorjs")
74
+ const EditorJS = mod.default
75
+ // Les packages tools Editor.js n'embarquent pas tous des types TS
76
+ // @ts-ignore
77
+ const Header = (await import("@editorjs/header")).default
78
+ // @ts-ignore
79
+ const Paragraph = (await import("@editorjs/paragraph")).default
80
+ // @ts-ignore
81
+ const List = (await import("@editorjs/list")).default
82
+ // @ts-ignore
83
+ const Checklist = (await import("@editorjs/checklist")).default
84
+ // @ts-ignore
85
+ const Quote = (await import("@editorjs/quote")).default
86
+ // @ts-ignore
87
+ const Code = (await import("@editorjs/code")).default
88
+ // @ts-ignore
89
+ const Delimiter = (await import("@editorjs/delimiter")).default
90
+
91
+ const defaults: Record<string, any> = {
92
+ paragraph: { class: Paragraph },
93
+ header: { class: Header, inlineToolbar: true, config: { levels: [1, 2, 3] } },
94
+ list: { class: List, inlineToolbar: true },
95
+ checklist: { class: Checklist, inlineToolbar: true },
96
+ quote: { class: Quote, inlineToolbar: true },
97
+ code: { class: Code },
98
+ delimiter: { class: Delimiter },
99
+ }
100
+
101
+ editor = new EditorJS({
102
+ holder: holderEl.value as HTMLElement,
103
+ tools: { ...defaults, ...props.tools },
104
+ data: props.data ?? undefined,
105
+ placeholder: props.placeholder,
106
+ readOnly: !isEditable(),
107
+ onChange: () => { emitData() },
108
+ onReady: () => { emit("ready", editor) },
109
+ })
110
+ emit("ready", editor)
111
+ })
112
+
113
+ // Changement externe → render (sans boucle : on compare au dernier JSON émis)
114
+ watch(
115
+ () => props.data,
116
+ (value) => {
117
+ if (internal || !editor) return
118
+ const json = value ? JSON.stringify(value) : ""
119
+ if (json === lastJson) return
120
+ editor.render(value ?? { time: Date.now(), blocks: [], version: "2.31.6" })
121
+ .then(() => { lastJson = json })
122
+ .catch(() => { /* ignore */ })
123
+ },
124
+ )
125
+
126
+ onBeforeUnmount(() => {
127
+ if (editor && typeof editor.destroy === "function") editor.destroy()
128
+ editor = null
129
+ })
130
+
131
+ // — Méthodes exposées —
132
+ const save = () => editor?.save() as Promise<QEditorJsData> | undefined
133
+ const clear = () => editor?.clear()
134
+ const getEditor = () => editor
135
+
136
+ defineExpose({ save, clear, getEditor })
137
+ </script>
138
+
139
+ <template>
140
+ <div
141
+ class="q-editor-js"
142
+ :class="{ 'q-editor-js--readonly': readonly || disable, 'q-editor-js--disable': disable }"
143
+ >
144
+ <div ref="holderEl" class="q-editor-js__holder" :style="{ minHeight }" />
145
+ </div>
146
+ </template>
@@ -10,7 +10,7 @@ export const qFabKey: InjectionKey<QFabContext> = Symbol("q-fab")
10
10
  </script>
11
11
 
12
12
  <script setup lang="ts">
13
- import { computed, provide, ref } from "vue"
13
+ import { computed, onBeforeUnmount, onMounted, provide, ref } from "vue"
14
14
  import { Icon } from "@iconify/vue"
15
15
  import { icons } from "../lib/icons"
16
16
  import { colorValue, foregroundFor } from "../lib/colors"
@@ -48,6 +48,23 @@ const toggle = () => {
48
48
  if (!props.disable) open.value = !open.value
49
49
  }
50
50
 
51
+ // Clic à l'extérieur du FAB → fermeture (actions repliées)
52
+ const rootEl = ref<HTMLElement | null>(null)
53
+ const onDocPointerDown = (e: PointerEvent) => {
54
+ if (!props.modelValue || !rootEl.value) return
55
+ if (!rootEl.value.contains(e.target as Node)) open.value = false
56
+ }
57
+ onMounted(() => {
58
+ if (typeof document !== "undefined") {
59
+ document.addEventListener("pointerdown", onDocPointerDown)
60
+ }
61
+ })
62
+ onBeforeUnmount(() => {
63
+ if (typeof document !== "undefined") {
64
+ document.removeEventListener("pointerdown", onDocPointerDown)
65
+ }
66
+ })
67
+
51
68
  provide<QFabContext>(qFabKey, {
52
69
  close: () => {
53
70
  open.value = false
@@ -61,7 +78,7 @@ const mainStyle = computed<Record<string, string>>(() => ({
61
78
  </script>
62
79
 
63
80
  <template>
64
- <div class="q-fab" :class="[`q-fab--${position}`, open && 'q-fab--open']">
81
+ <div ref="rootEl" class="q-fab" :class="[`q-fab--${position}`, open && 'q-fab--open']">
65
82
  <div class="q-fab__actions" role="menu">
66
83
  <slot />
67
84
  </div>
@@ -63,15 +63,28 @@ const slotCode = computed(() => {
63
63
 
64
64
  const source = computed(() => props.code ?? slotCode.value)
65
65
 
66
- // Highlighter singleton : une seule instance pour toute l'application
67
- let highlighterPromise: Promise<Highlighter> | null = null
68
- const getHighlighter = () => {
69
- highlighterPromise ??= createHighlighter({
70
- themes: [DEFAULT_THEME],
71
- langs: DEFAULT_LANGS,
72
- engine: createJavaScriptRegexEngine(),
73
- })
74
- return highlighterPromise
66
+ // Highlighter singleton : une seule instance pour toute l'application.
67
+ // Stocké sur globalThis (et non au scope module) pour survivre aux
68
+ // réévaluations du module (HMR — édition de QSyntax.vue ou de ses imports,
69
+ // ex. lib/icons.ts) : sinon une instance orpheline est créée à chaque cycle
70
+ // et Shiki avertit (« [Shiki] N instances have been created… »).
71
+ const SHIKI_KEY = "__dnax_ui_shiki_highlighter__"
72
+ const getHighlighter = (): Promise<Highlighter> => {
73
+ const g = globalThis as { [SHIKI_KEY]?: Promise<Highlighter> }
74
+ let highlighter = g[SHIKI_KEY]
75
+ if (!highlighter) {
76
+ highlighter = createHighlighter({
77
+ themes: [DEFAULT_THEME],
78
+ langs: DEFAULT_LANGS,
79
+ engine: createJavaScriptRegexEngine(),
80
+ }).catch((error: unknown) => {
81
+ // Une création en échec ne doit pas bloquer les montages suivants
82
+ delete g[SHIKI_KEY]
83
+ throw error
84
+ })
85
+ g[SHIKI_KEY] = highlighter
86
+ }
87
+ return highlighter
75
88
  }
76
89
 
77
90
  const html = ref("")