@nexxtmove/ui 1.36.0 → 1.38.0

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,155 @@
1
+ import type { NexxtTreeNode } from './Tree.vue'
2
+
3
+ /** A visible row of the tree, in document order. */
4
+ export interface TreeRow {
5
+ node: NexxtTreeNode
6
+ depth: number
7
+ parentId: string | null
8
+ expandable: boolean
9
+ canHaveChildren: boolean
10
+ draggable: boolean
11
+ expanded: boolean
12
+ /** One-based position among its siblings. */
13
+ position: number
14
+ setSize: number
15
+ }
16
+
17
+ export type DropMarker = 'before' | 'inside' | 'after'
18
+
19
+ export interface TreeDrop {
20
+ /** The row the pointer was over when this spot was picked. */
21
+ rowId: string
22
+ parentId: string | null
23
+ index: number
24
+ marker: DropMarker
25
+ }
26
+
27
+ /**
28
+ * Where a row sits with the ghost row taken out, relative to the top of the tree. `bottom` takes
29
+ * in the gap below the row: a gap belongs to the row above it, so moving across it never reads
30
+ * as leaving the tree.
31
+ */
32
+ export interface RestSlot {
33
+ id: string
34
+ top: number
35
+ height: number
36
+ bottom: number
37
+ }
38
+
39
+ export interface DropLayout {
40
+ slots: RestSlot[]
41
+ /** The ghost row as it is laid out now, gap included; `null` while there is none. */
42
+ ghost: { top: number; bottom: number } | null
43
+ }
44
+
45
+ /** `keep` leaves the current spot as it is: the pointer is on the ghost row, or there are no rows. */
46
+ export type DropHit = { kind: 'keep' } | { kind: 'row'; id: string; ratio: number }
47
+
48
+ export const containsNode = (node: NexxtTreeNode, id: string): boolean =>
49
+ node.id === id || !!node.children?.some((child) => containsNode(child, id))
50
+
51
+ export const findNode = (nodes: NexxtTreeNode[], id: string): NexxtTreeNode | null => {
52
+ for (const node of nodes) {
53
+ if (node.id === id) return node
54
+
55
+ const found = node.children ? findNode(node.children, id) : null
56
+ if (found) return found
57
+ }
58
+
59
+ return null
60
+ }
61
+
62
+ /**
63
+ * Reads the pointer against where the rows land without the ghost row, not against what is under
64
+ * it now. The ghost pushes rows aside, so reading what is under the pointer would find a shifted
65
+ * row or the ghost itself, pick another spot, move the ghost and flicker. Measured this way the
66
+ * same pointer always gives the same answer, however the ghost moves.
67
+ *
68
+ * @param layout - The rows at rest and the ghost row as it is laid out now.
69
+ * @param y - The pointer, relative to the top of the tree.
70
+ */
71
+ export const hitTest = ({ slots, ghost }: DropLayout, y: number): DropHit => {
72
+ if (ghost && y >= ghost.top && y < ghost.bottom) return { kind: 'keep' }
73
+
74
+ const restY = ghost && y >= ghost.bottom ? y - (ghost.bottom - ghost.top) : y
75
+ const last = slots.at(-1)
76
+
77
+ // The empty space a tree can have below its last row is the end of that row's level, not a gap
78
+ if (last && restY >= last.bottom) return { kind: 'row', id: last.id, ratio: 1 }
79
+
80
+ const slot = slots.find((item) => restY >= item.top && restY < item.bottom)
81
+ if (!slot) return { kind: 'keep' }
82
+
83
+ const ratio = slot.height ? (restY - slot.top) / slot.height : 0
84
+
85
+ return { kind: 'row', id: slot.id, ratio: Math.min(1, Math.max(0, ratio)) }
86
+ }
87
+
88
+ /**
89
+ * The spot a pointer at `ratio` of `row` stands for. A group splits into before (top 30%),
90
+ * inside and after (bottom 30%), a leaf into before and after at the middle. Inside always lands
91
+ * as the first child, right under the group row, so the ghost shows up next to the pointer. The
92
+ * dragged node and everything inside it are no target at all.
93
+ */
94
+ export const resolveDrop = (
95
+ row: TreeRow,
96
+ ratio: number,
97
+ moving: NexxtTreeNode,
98
+ ): TreeDrop | null => {
99
+ if (containsNode(moving, row.node.id)) return null
100
+
101
+ if (row.canHaveChildren && ratio > 0.3 && ratio < 0.7) {
102
+ return { rowId: row.node.id, parentId: row.node.id, index: 0, marker: 'inside' }
103
+ }
104
+
105
+ const after = ratio >= 0.5
106
+
107
+ return {
108
+ rowId: row.node.id,
109
+ parentId: row.parentId,
110
+ index: row.position - 1 + (after ? 1 : 0),
111
+ marker: after ? 'after' : 'before',
112
+ }
113
+ }
114
+
115
+ /** A spot where the dragged row already is: dropping there changes nothing, so it is no target. */
116
+ export const isNoOp = (drop: TreeDrop, source: TreeRow) =>
117
+ drop.parentId === source.parentId &&
118
+ (drop.index === source.position - 1 || drop.index === source.position)
119
+
120
+ /** Two spots are the same when they land the node in the same place, whichever row picked them. */
121
+ export const samePlace = (a: TreeDrop | null, b: TreeDrop | null) =>
122
+ a === b || (!!a && !!b && a.parentId === b.parentId && a.index === b.index)
123
+
124
+ /**
125
+ * Where the ghost row goes among the visible rows, and how deep. Right before the row of the
126
+ * sibling now at `index`, or after the last visible row inside the parent when `index` is past
127
+ * its last child. `null` for a spot inside a collapsed group: that group row marks itself.
128
+ */
129
+ export const ghostSlot = (
130
+ rows: TreeRow[],
131
+ nodes: NexxtTreeNode[],
132
+ drop: TreeDrop | null,
133
+ ): { at: number; depth: number } | null => {
134
+ if (!drop) return null
135
+
136
+ const parentAt =
137
+ drop.parentId === null ? -1 : rows.findIndex((row) => row.node.id === drop.parentId)
138
+ const parent = rows[parentAt]
139
+ if (drop.parentId !== null && !parent) return null
140
+ if (parent?.expandable && !parent.expanded) return null
141
+
142
+ const depth = parent ? parent.depth + 1 : 0
143
+ const next = (parent ? (parent.node.children ?? []) : nodes)[drop.index]
144
+
145
+ if (next) {
146
+ const at = rows.findIndex((row) => row.node.id === next.id)
147
+
148
+ return at === -1 ? null : { at, depth }
149
+ }
150
+
151
+ let at = parentAt + 1
152
+ while (at < rows.length && rows[at]!.depth >= depth) at++
153
+
154
+ return { at, depth }
155
+ }
@@ -80,6 +80,8 @@
80
80
  "NexxtEditorGap": "components/editor/molecules/EditorGap/EditorGap.vue",
81
81
  "NexxtEditorHeadingType": "components/editor/molecules/EditorHeadingType/EditorHeadingType.vue",
82
82
  "NexxtEditorJustifyContent": "components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue",
83
+ "NexxtEditorLetterSpacing": "components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue",
84
+ "NexxtEditorLineHeight": "components/editor/molecules/EditorLineHeight/EditorLineHeight.vue",
83
85
  "NexxtEditorMaxWidth": "components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue",
84
86
  "NexxtEditorPadding": "components/editor/molecules/EditorPadding/EditorPadding.vue",
85
87
  "NexxtEditorRadius": "components/editor/molecules/EditorRadius/EditorRadius.vue",
package/src/env.d.ts CHANGED
@@ -1,6 +1,11 @@
1
1
  /// <reference types="vite/client" />
2
2
 
3
- declare module '*.svg' {
3
+ // vite-svg-loader compiles a plain SVG import to a Vue component, but
4
+ // vite/client above declares `*.svg` as a URL string, and of two identical
5
+ // wildcard patterns the one loaded first wins. TypeScript picks the pattern
6
+ // with the longest prefix, so the library's own `@ui-assets` SVGs get their
7
+ // component type here regardless of load order.
8
+ declare module '@ui-assets/*.svg' {
4
9
  import type { DefineComponent } from 'vue'
5
10
  const component: DefineComponent<object, object, unknown>
6
11
  export default component
package/src/index.ts CHANGED
@@ -23,6 +23,7 @@ export type {
23
23
  NexxtPickerItemData,
24
24
  } from './components/molecules/PickerList/PickerList.vue'
25
25
  export type {
26
+ NexxtTreeMove,
26
27
  NexxtTreeNode,
27
28
  NexxtTreeProps,
28
29
  NexxtTreeSlots,
@@ -83,6 +84,16 @@ export type {
83
84
  EditorMaxWidthMessages,
84
85
  EditorMaxWidthValue,
85
86
  } from './components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue'
87
+ export { EDITOR_LINE_HEIGHT_STEPS } from './components/editor/molecules/EditorLineHeight/EditorLineHeight.vue'
88
+ export type {
89
+ EditorLineHeightMessages,
90
+ EditorLineHeightValue,
91
+ } from './components/editor/molecules/EditorLineHeight/EditorLineHeight.vue'
92
+ export { EDITOR_LETTER_SPACING_STEPS } from './components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue'
93
+ export type {
94
+ EditorLetterSpacingMessages,
95
+ EditorLetterSpacingValue,
96
+ } from './components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue'
86
97
  export type { EditorPaddingMessages } from './components/editor/molecules/EditorPadding/EditorPadding.vue'
87
98
  export type {
88
99
  NexxtTemplateCardContentFeature,
@@ -115,6 +126,8 @@ export { default as NexxtEditorFlexDirection } from './components/editor/molecul
115
126
  export { default as NexxtEditorGap } from './components/editor/molecules/EditorGap/EditorGap.vue'
116
127
  export { default as NexxtEditorHeadingType } from './components/editor/molecules/EditorHeadingType/EditorHeadingType.vue'
117
128
  export { default as NexxtEditorJustifyContent } from './components/editor/molecules/EditorJustifyContent/EditorJustifyContent.vue'
129
+ export { default as NexxtEditorLetterSpacing } from './components/editor/molecules/EditorLetterSpacing/EditorLetterSpacing.vue'
130
+ export { default as NexxtEditorLineHeight } from './components/editor/molecules/EditorLineHeight/EditorLineHeight.vue'
118
131
  export { default as NexxtEditorMaxWidth } from './components/editor/molecules/EditorMaxWidth/EditorMaxWidth.vue'
119
132
  export { default as NexxtEditorPadding } from './components/editor/molecules/EditorPadding/EditorPadding.vue'
120
133
  export { default as NexxtEditorRadius } from './components/editor/molecules/EditorRadius/EditorRadius.vue'