@globalbrain/sefirot 4.34.1 → 4.35.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.
Files changed (55) hide show
  1. package/config/nuxt.js +44 -1
  2. package/config/vite.js +2 -3
  3. package/lib/blocks/lens/FieldContext.ts +5 -0
  4. package/lib/blocks/lens/FieldData.ts +140 -0
  5. package/lib/blocks/lens/FieldRegistry.ts +23 -0
  6. package/lib/blocks/lens/FileDownloader.ts +1 -0
  7. package/lib/blocks/lens/FilterOperator.ts +33 -0
  8. package/lib/blocks/lens/LensQuery.ts +10 -0
  9. package/lib/blocks/lens/LensResult.ts +20 -0
  10. package/lib/blocks/lens/ResourceFetcher.ts +3 -0
  11. package/lib/blocks/lens/Rule.ts +12 -0
  12. package/lib/blocks/lens/components/LensCatalog.vue +490 -0
  13. package/lib/blocks/lens/components/LensCatalogControl.vue +220 -0
  14. package/lib/blocks/lens/components/LensCatalogFooter.vue +46 -0
  15. package/lib/blocks/lens/components/LensCatalogStateFilter.vue +171 -0
  16. package/lib/blocks/lens/components/LensCatalogStateFilterCondition.vue +86 -0
  17. package/lib/blocks/lens/components/LensCatalogStateFilterGroup.vue +102 -0
  18. package/lib/blocks/lens/components/LensCatalogStateSort.vue +159 -0
  19. package/lib/blocks/lens/components/LensFormFilter.vue +169 -0
  20. package/lib/blocks/lens/components/LensFormFilterCondition.vue +205 -0
  21. package/lib/blocks/lens/components/LensFormFilterGroup.vue +175 -0
  22. package/lib/blocks/lens/components/LensFormOverride.vue +45 -0
  23. package/lib/blocks/lens/components/LensFormOverrideBase.vue +204 -0
  24. package/lib/blocks/lens/components/LensFormView.vue +347 -0
  25. package/lib/blocks/lens/components/LensTable.vue +154 -0
  26. package/lib/blocks/lens/composables/FieldFactory.ts +27 -0
  27. package/lib/blocks/lens/composables/FieldRegistry.ts +16 -0
  28. package/lib/blocks/lens/composables/FileDownloader.ts +10 -0
  29. package/lib/blocks/lens/composables/ResourceFetcher.ts +30 -0
  30. package/lib/blocks/lens/composables/SetupLens.ts +55 -0
  31. package/lib/blocks/lens/fields/ContentField.ts +34 -0
  32. package/lib/blocks/lens/fields/DateField.ts +66 -0
  33. package/lib/blocks/lens/fields/DatetimeField.ts +35 -0
  34. package/lib/blocks/lens/fields/Field.ts +244 -0
  35. package/lib/blocks/lens/fields/FileUploadField.ts +63 -0
  36. package/lib/blocks/lens/fields/IdField.ts +34 -0
  37. package/lib/blocks/lens/fields/LinkField.ts +53 -0
  38. package/lib/blocks/lens/fields/NumberField.ts +32 -0
  39. package/lib/blocks/lens/fields/RelatedManyField.ts +62 -0
  40. package/lib/blocks/lens/fields/SelectField.ts +198 -0
  41. package/lib/blocks/lens/fields/SlackMessageField.ts +34 -0
  42. package/lib/blocks/lens/fields/TextField.ts +46 -0
  43. package/lib/blocks/lens/fields/TextareaField.ts +49 -0
  44. package/lib/blocks/lens/filter-inputs/FilterInput.ts +72 -0
  45. package/lib/blocks/lens/filter-inputs/NumberFilterInput.ts +26 -0
  46. package/lib/blocks/lens/filter-inputs/SelectFilterInput.ts +76 -0
  47. package/lib/blocks/lens/filter-inputs/TextFilterInput.ts +26 -0
  48. package/lib/blocks/lens/validation/RuleMapper.ts +22 -0
  49. package/lib/components/SInputTextarea.vue +28 -10
  50. package/lib/components/STable.vue +230 -61
  51. package/lib/components/STableCell.vue +2 -2
  52. package/lib/composables/TableAnimation.ts +180 -0
  53. package/lib/support/Scroll.ts +263 -0
  54. package/lib/support/Utils.ts +1 -1
  55. package/package.json +7 -15
@@ -0,0 +1,263 @@
1
+ // The browser-native smooth scroll animation is too soft and slow, too catchy.
2
+ // This module provides a custom smooth scroll that is more subtle and faster.
3
+
4
+ // Duration configuration (in milliseconds)
5
+ const MIN_SCROLL_DURATION = 100
6
+ const MAX_SCROLL_DURATION = 160
7
+ const DURATION_SCALE_FACTOR = 0.15
8
+ const TOP_OFFSET_TOLERANCE_PX = 10
9
+ type ScrollTarget = HTMLElement | Window
10
+
11
+ const topOffsetCache = new WeakMap<HTMLElement, number>()
12
+
13
+ function easeOutCubic(t: number): number {
14
+ return 1 - (1 - t) ** 3
15
+ }
16
+
17
+ /**
18
+ * Calculate scroll animation duration based on distance.
19
+ * Shorter distances get faster animations, longer distances are capped at max duration.
20
+ */
21
+ function calculateScrollDuration(distance: number): number {
22
+ const absDistance = Math.abs(distance)
23
+ const scaled = absDistance * DURATION_SCALE_FACTOR
24
+ return Math.min(MAX_SCROLL_DURATION, Math.max(MIN_SCROLL_DURATION, scaled))
25
+ }
26
+
27
+ function getScrollTop(target: ScrollTarget): number {
28
+ return target instanceof Window ? target.scrollY : target.scrollTop
29
+ }
30
+
31
+ function setScrollTop(target: ScrollTarget, value: number): void {
32
+ if (target instanceof Window) {
33
+ target.scrollTo(0, value)
34
+ } else {
35
+ target.scrollTop = value
36
+ }
37
+ }
38
+
39
+ function getEventTarget(target: ScrollTarget): EventTarget {
40
+ return target instanceof Window ? window : target
41
+ }
42
+
43
+ /**
44
+ * Smoothly scroll an element to a target Y position with adaptive duration.
45
+ * The animation can be interrupted by user interaction (wheel, touch, mouse, keyboard).
46
+ */
47
+ function smoothScrollTo(target: ScrollTarget, targetY: number): Promise<void> {
48
+ const from = getScrollTop(target)
49
+ const delta = targetY - from
50
+
51
+ // Skip animation for very small distances
52
+ if (Math.abs(delta) < 1) {
53
+ return Promise.resolve()
54
+ }
55
+
56
+ const duration = calculateScrollDuration(delta)
57
+ let startTime: number | null = null
58
+ let animationFrame: number | null = null
59
+ let resolvePromise: () => void = () => undefined
60
+ let settled = false
61
+
62
+ // Allow user to interrupt the scroll
63
+ const controller = new AbortController()
64
+ const finishScroll = () => {
65
+ if (settled) {
66
+ return
67
+ }
68
+ settled = true
69
+ if (animationFrame !== null) {
70
+ cancelAnimationFrame(animationFrame)
71
+ animationFrame = null
72
+ }
73
+ controller.abort()
74
+ resolvePromise()
75
+ }
76
+ const cancel = () => {
77
+ if (controller.signal.aborted) {
78
+ return
79
+ }
80
+ finishScroll()
81
+ }
82
+
83
+ const opts = { passive: true, signal: controller.signal } as const
84
+
85
+ const eventTarget = getEventTarget(target)
86
+ eventTarget.addEventListener('wheel', cancel, opts)
87
+ eventTarget.addEventListener('touchstart', cancel, opts)
88
+ eventTarget.addEventListener('mousedown', cancel, opts)
89
+ eventTarget.addEventListener('keydown', cancel, opts)
90
+
91
+ return new Promise<void>((resolve) => {
92
+ resolvePromise = resolve
93
+
94
+ function step(currentTime: number): void {
95
+ if (controller.signal.aborted) {
96
+ finishScroll()
97
+ return
98
+ }
99
+
100
+ if (startTime === null) {
101
+ startTime = currentTime
102
+ }
103
+
104
+ const elapsed = currentTime - startTime
105
+ const progress = Math.min(elapsed / duration, 1)
106
+ const eased = easeOutCubic(progress)
107
+ const position = from + delta * eased
108
+
109
+ setScrollTop(target, position)
110
+
111
+ if (progress < 1) {
112
+ animationFrame = requestAnimationFrame(step)
113
+ } else {
114
+ animationFrame = null
115
+ finishScroll()
116
+ }
117
+ }
118
+
119
+ animationFrame = requestAnimationFrame(step)
120
+ })
121
+ }
122
+
123
+ /**
124
+ * Detect fixed or sticky elements at the top of the viewport and calculate
125
+ * the total height they occupy.
126
+ */
127
+ function detectTopOffset(tableRootElement: HTMLElement): number {
128
+ if (typeof document === 'undefined') {
129
+ return 0
130
+ }
131
+
132
+ if (topOffsetCache.has(tableRootElement)) {
133
+ return topOffsetCache.get(tableRootElement) ?? 0
134
+ }
135
+
136
+ let maxBottom = 0
137
+ const elements = document.querySelectorAll('*')
138
+
139
+ for (const el of elements) {
140
+ const styles = window.getComputedStyle(el)
141
+ const position = styles.position
142
+
143
+ // Only consider fixed or sticky elements
144
+ if (position !== 'fixed' && position !== 'sticky') {
145
+ continue
146
+ }
147
+
148
+ const rect = el.getBoundingClientRect()
149
+
150
+ // Check if element is positioned at or near the top
151
+ // and is visible
152
+ if (
153
+ rect.top <= TOP_OFFSET_TOLERANCE_PX
154
+ && rect.height > 0
155
+ && rect.width > 0
156
+ ) {
157
+ // Track the furthest bottom edge
158
+ maxBottom = Math.max(maxBottom, rect.bottom)
159
+ }
160
+ }
161
+
162
+ const computed = maxBottom > 0 ? maxBottom : 0
163
+ topOffsetCache.set(tableRootElement, computed)
164
+ return computed
165
+ }
166
+
167
+ function isAlreadyVisible(
168
+ relativeTop: number,
169
+ containerHeight: number,
170
+ topOffset: number
171
+ ): boolean {
172
+ const isNotCoveredByHeader = relativeTop >= topOffset
173
+ const isInView = relativeTop < containerHeight
174
+ return isNotCoveredByHeader && isInView
175
+ }
176
+
177
+ function getTargetScrollTop(
178
+ currentScrollTop: number,
179
+ relativeTop: number,
180
+ borderOffset: number,
181
+ topOffset: number
182
+ ): number {
183
+ return currentScrollTop + relativeTop - borderOffset - topOffset
184
+ }
185
+
186
+ /**
187
+ * Scroll the page so the table is positioned at the top of the viewport.
188
+ * Finds the nearest scrollable parent or scrolls the window if needed.
189
+ * Automatically accounts for fixed/sticky headers at the top.
190
+ */
191
+ export function scrollTableIntoView(
192
+ tableRootElement: HTMLElement,
193
+ headElement: HTMLElement | null,
194
+ bodyElement: HTMLElement | null,
195
+ borderless: boolean,
196
+ borderSize: number
197
+ ): Promise<void> {
198
+ const rect = tableRootElement.getBoundingClientRect()
199
+ const borderOffset = borderless ? 0 : borderSize
200
+ const topOffset = detectTopOffset(tableRootElement)
201
+
202
+ const resetHorizontalScroll = () => {
203
+ if (headElement) {
204
+ headElement.scrollLeft = 0
205
+ }
206
+ if (bodyElement) {
207
+ bodyElement.scrollLeft = 0
208
+ }
209
+ }
210
+
211
+ // Smooth scroll the table body to the top
212
+ const bodyScrollToTop = bodyElement
213
+ ? smoothScrollTo(bodyElement, 0)
214
+ : Promise.resolve()
215
+ const resetAfterBodyScrollToTop = () =>
216
+ bodyScrollToTop.then(() => resetHorizontalScroll())
217
+ const runContainerScrollAndReset = (containerScrollToTable: Promise<void>) =>
218
+ Promise.all([containerScrollToTable, bodyScrollToTop]).then(() =>
219
+ resetHorizontalScroll()
220
+ )
221
+
222
+ // Try to find a scrollable parent container
223
+ let scrollableParent = tableRootElement.parentElement
224
+ while (scrollableParent) {
225
+ const overflowY = window.getComputedStyle(scrollableParent).overflowY
226
+ const isScrollable = overflowY === 'auto' || overflowY === 'scroll'
227
+
228
+ if (isScrollable) {
229
+ const parentRect = scrollableParent.getBoundingClientRect()
230
+ const relativeTop = rect.top - parentRect.top
231
+
232
+ if (isAlreadyVisible(relativeTop, parentRect.height, topOffset)) {
233
+ return resetAfterBodyScrollToTop()
234
+ }
235
+
236
+ const targetScrollTop = getTargetScrollTop(
237
+ scrollableParent.scrollTop,
238
+ relativeTop,
239
+ borderOffset,
240
+ topOffset
241
+ )
242
+ return runContainerScrollAndReset(
243
+ smoothScrollTo(scrollableParent, targetScrollTop)
244
+ )
245
+ }
246
+
247
+ scrollableParent = scrollableParent.parentElement
248
+ }
249
+
250
+ // No scrollable parent found, check window scroll
251
+
252
+ if (isAlreadyVisible(rect.top, window.innerHeight, topOffset)) {
253
+ return resetAfterBodyScrollToTop()
254
+ }
255
+
256
+ const windowScrollTop = getTargetScrollTop(
257
+ window.scrollY,
258
+ rect.top,
259
+ borderOffset,
260
+ topOffset
261
+ )
262
+ return runContainerScrollAndReset(smoothScrollTo(window, windowScrollTop))
263
+ }
@@ -3,7 +3,7 @@ export function isNumber(value: unknown): value is number {
3
3
  return Number.isFinite(value)
4
4
  }
5
5
 
6
- export function isObject(value: unknown): value is Record<string, unknown> {
6
+ export function isObject(value: unknown): value is Record<PropertyKey, unknown> {
7
7
  if (value == null || typeof value !== 'object') { return false }
8
8
 
9
9
  const proto = Object.getPrototypeOf(value)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@globalbrain/sefirot",
3
- "version": "4.34.1",
3
+ "version": "4.35.0",
4
4
  "description": "Vue Components for Global Brain Design System.",
5
5
  "keywords": [
6
6
  "vue",
@@ -19,25 +19,15 @@
19
19
  "author": "Kia Ishii <ka.ishii@globalbrains.com>",
20
20
  "type": "module",
21
21
  "exports": {
22
- "./client": {
23
- "types": "./client.d.ts"
24
- },
25
- "./config/nuxt": {
26
- "types": "./config/nuxt.d.ts",
27
- "import": "./config/nuxt.js"
28
- },
29
- "./config/vite": {
30
- "types": "./config/vite.d.ts",
31
- "import": "./config/vite.js"
32
- },
22
+ "./client": "./client.d.ts",
23
+ "./shared": "./shared.d.ts",
24
+ "./config/nuxt": "./config/nuxt.js",
25
+ "./config/vite": "./config/vite.js",
33
26
  "./dompurify": {
34
27
  "types": "./lib/dompurify/index.d.ts",
35
28
  "browser": "./lib/dompurify/browser.js",
36
29
  "default": "./lib/dompurify/node.js"
37
30
  },
38
- "./shared": {
39
- "types": "./shared.d.ts"
40
- },
41
31
  "./*": "./*"
42
32
  },
43
33
  "files": [
@@ -87,6 +77,7 @@
87
77
  "postcss-nested": "^7.0.2",
88
78
  "v-calendar": "3.0.1",
89
79
  "vue": "^3.5.27",
80
+ "vue-draggable-plus": "^0.6.0",
90
81
  "vue-router": "^4.6.4"
91
82
  },
92
83
  "dependencies": {
@@ -148,6 +139,7 @@
148
139
  "vitepress": "^2.0.0-alpha.15",
149
140
  "vitest": "^3.2.4",
150
141
  "vue": "^3.5.27",
142
+ "vue-draggable-plus": "^0.6.0",
151
143
  "vue-router": "^4.6.4",
152
144
  "vue-tsc": "^3.2.2"
153
145
  },