@raclettejs/core 0.1.17 → 0.1.19

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 (49) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/package.json +2 -2
  3. package/services/backend/package.json +2 -2
  4. package/services/backend/src/core/config/configService.ts +4 -0
  5. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/composition.service.ts +86 -0
  6. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/index.ts +4 -0
  7. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.post.bulk.ts +58 -0
  8. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/composition/routes/route.composition.restore.ts +55 -0
  9. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/interactionLink.service.ts +96 -9
  10. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/index.ts +4 -0
  11. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.post.bulk.ts +60 -0
  12. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/route.interactionLink.restore.ts +55 -0
  13. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.model.ts +17 -0
  14. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/project.schema.ts +18 -6
  15. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/project/routes/index.ts +5 -5
  16. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/index.ts +4 -0
  17. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.post.bulk.ts +58 -0
  18. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/routes/route.tag.restore.ts +55 -0
  19. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/tag/tag.service.ts +74 -1
  20. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/index.ts +2 -0
  21. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.restore.ts +55 -0
  22. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/user.service.ts +1 -0
  23. package/services/backend/src/domains/system/routes/route.user.login.ts +4 -2
  24. package/services/backend/src/shared/types/core/ProjectUpdate.types.ts +1 -0
  25. package/services/backend/src/shared/types/dataTypes/tags.ts +1 -0
  26. package/services/frontend/package.json +8 -8
  27. package/services/frontend/src/core/lib/configService.ts +1 -1
  28. package/services/frontend/src/core/lib/data/dataApi.ts +10 -1
  29. package/services/frontend/src/core/lib/data/fetchDataHandler.ts +3 -23
  30. package/services/frontend/src/core/lib/data/writeDataHandler.ts +8 -13
  31. package/services/frontend/src/core/lib/dataHelper.ts +15 -0
  32. package/services/frontend/src/core/lib/eggs/snow.ts +32 -43
  33. package/services/frontend/src/core/lib/eventWhitelist.ts +4 -13
  34. package/services/frontend/src/core/main.ts +7 -1
  35. package/services/frontend/src/orchestrator/LoginApp.vue +0 -1
  36. package/services/frontend/src/orchestrator/ProductOrchestrator.vue +40 -5
  37. package/services/frontend/src/orchestrator/components/composition/CompositionOverlay.vue +7 -3
  38. package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +12 -24
  39. package/services/frontend/src/orchestrator/components/input/SimpleSearchInput.vue +16 -4
  40. package/services/frontend/src/orchestrator/components/menu/AppBarMenu.vue +184 -0
  41. package/services/frontend/src/orchestrator/components/menu/UserMenu.vue +62 -46
  42. package/services/frontend/src/orchestrator/components/navigation/PageNavigationSidebar.vue +11 -1
  43. package/services/frontend/src/orchestrator/composables/usePageNavigation.ts +10 -1
  44. package/services/frontend/src/orchestrator/i18n/de-DE.json +15 -0
  45. package/services/frontend/src/orchestrator/i18n/en-EU.json +15 -0
  46. package/services/frontend/src/orchestrator/i18n/sk.json +15 -0
  47. package/services/frontend/src/orchestrator/router/index.ts +16 -0
  48. package/yarn.lock +4 -4
  49. package/services/frontend/src/orchestrator/setup/loginApp.ts +0 -27
@@ -7,11 +7,13 @@ export default () => {
7
7
 
8
8
  // This function allows you to turn on and off the snow
9
9
  const toggle_snow = () => {
10
- const check_box = document.getElementById("toggle_snow")
10
+ const check_box = document.getElementById(
11
+ "toggle_snow",
12
+ ) as HTMLInputElement | null
11
13
  const snowEl = document.getElementById("snow")
12
14
 
13
15
  if (snowEl) {
14
- snowEl.style.display = check_box.checked ? "block" : "none"
16
+ snowEl.style.display = check_box?.checked ? "block" : "none"
15
17
  }
16
18
  }
17
19
 
@@ -29,87 +31,74 @@ export default () => {
29
31
  const randomDuration = 8 + Math.random() * 15 // Duration between 8-23s
30
32
  const randomDelay = Math.random() * 10 // Delay between 0-10s
31
33
  const randomOpacity = 0.3 + Math.random() * 0.7 // Opacity between 0.3-1
32
- const randomAnimation = Math.floor(Math.random() * 5) + 1 // Choose from 5 animations
34
+ const randomAnimation = Math.floor(Math.random() * 3) + 1 // Choose from 3 animations
33
35
 
34
- snowflake.style.setProperty("--start-x", `${randomX}vw`)
35
- snowflake.style.setProperty("--scale", randomScale)
36
+ snowflake.style.setProperty("--start-x", `${randomX}%`)
37
+ snowflake.style.setProperty("--scale", randomScale.toString())
36
38
  snowflake.style.setProperty("--duration", `${randomDuration}s`)
37
39
  snowflake.style.setProperty("--delay", `${randomDelay}s`)
38
- snowflake.style.setProperty("--opacity", randomOpacity)
40
+ snowflake.style.setProperty("--opacity", randomOpacity.toString())
39
41
  snowflake.style.setProperty("--animation-name", `fall-${randomAnimation}`)
40
42
 
41
43
  fragment.appendChild(snowflake)
42
44
  }
43
45
 
44
- document.getElementById("snow").appendChild(fragment)
46
+ document.getElementById("snow")?.appendChild(fragment)
45
47
  }
46
48
 
47
- // Optimized CSS with reusable animations and CSS custom properties
49
+ // Optimized CSS with GPU acceleration and simplified animations
48
50
  const createOptimizedCSS = () => {
49
51
  const css = `
50
52
  #snow {
51
53
  position: fixed;
52
54
  top: 0;
53
55
  left: 0;
54
- width: 100%;
55
- height: 100%;
56
+ right: 0;
57
+ bottom: 0;
56
58
  pointer-events: none;
57
59
  z-index: 1000;
60
+ overflow: hidden;
58
61
  }
59
62
 
60
63
  .snowflake {
61
64
  position: absolute;
65
+ top: -10px;
62
66
  width: 8px;
63
67
  height: 8px;
64
68
  background: white;
65
69
  border-radius: 50%;
66
- filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.8));
70
+
71
+ /* GPU acceleration - critical for performance */
67
72
  will-change: transform;
73
+ transform: translateZ(0);
74
+ backface-visibility: hidden;
75
+
76
+ /* Simplified glow effect - much lighter than drop-shadow filter */
77
+ box-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
68
78
 
69
79
  /* Use CSS custom properties for individual variations */
70
80
  left: var(--start-x);
71
81
  opacity: var(--opacity);
72
- transform: scale(var(--scale));
73
82
  animation: var(--animation-name) var(--duration) var(--delay) linear infinite;
74
83
  }
75
84
 
76
- /* 5 different falling patterns for variety */
85
+ /* Simplified to 3 falling patterns - uses only transform for best performance */
77
86
  @keyframes fall-1 {
78
- 0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
79
- 25% { transform: translateY(25vh) translateX(10px) scale(var(--scale)); }
80
- 50% { transform: translateY(50vh) translateX(-5px) scale(var(--scale)); }
81
- 75% { transform: translateY(75vh) translateX(8px) scale(var(--scale)); }
82
- 100% { transform: translateY(100vh) translateX(0) scale(var(--scale)); }
87
+ to {
88
+ transform: translateY(100vh) translateX(20px) scale(var(--scale)) translateZ(0);
89
+ }
83
90
  }
84
91
 
85
92
  @keyframes fall-2 {
86
- 0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
87
- 30% { transform: translateY(30vh) translateX(-15px) scale(var(--scale)); }
88
- 60% { transform: translateY(60vh) translateX(10px) scale(var(--scale)); }
89
- 100% { transform: translateY(100vh) translateX(-5px) scale(var(--scale)); }
93
+ to {
94
+ transform: translateY(100vh) translateX(-20px) scale(var(--scale)) translateZ(0);
95
+ }
90
96
  }
91
97
 
92
98
  @keyframes fall-3 {
93
- 0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
94
- 20% { transform: translateY(20vh) translateX(5px) scale(var(--scale)); }
95
- 40% { transform: translateY(40vh) translateX(-10px) scale(var(--scale)); }
96
- 70% { transform: translateY(70vh) translateX(15px) scale(var(--scale)); }
97
- 100% { transform: translateY(100vh) translateX(0) scale(var(--scale)); }
98
- }
99
-
100
- @keyframes fall-4 {
101
- 0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
102
- 35% { transform: translateY(35vh) translateX(20px) scale(var(--scale)); }
103
- 65% { transform: translateY(65vh) translateX(-8px) scale(var(--scale)); }
104
- 100% { transform: translateY(100vh) translateX(12px) scale(var(--scale)); }
105
- }
106
-
107
- @keyframes fall-5 {
108
- 0% { transform: translateY(-10px) translateX(0) scale(var(--scale)); }
109
- 15% { transform: translateY(15vh) translateX(-5px) scale(var(--scale)); }
110
- 45% { transform: translateY(45vh) translateX(12px) scale(var(--scale)); }
111
- 80% { transform: translateY(80vh) translateX(-7px) scale(var(--scale)); }
112
- 100% { transform: translateY(100vh) translateX(3px) scale(var(--scale)); }
99
+ to {
100
+ transform: translateY(100vh) translateX(0px) scale(var(--scale)) translateZ(0);
101
+ }
113
102
  }
114
103
  `
115
104
 
@@ -142,7 +131,7 @@ export default () => {
142
131
  } else {
143
132
  // Remove existing snow
144
133
  snowEl.remove()
145
- // Optionally remove styles as well
134
+ // Remove styles as well
146
135
  const styles = document.getElementById("snow-styles")
147
136
  if (styles) {
148
137
  styles.remove()
@@ -6,19 +6,9 @@
6
6
  * change of state, others represent a request addressed to a specific actor,
7
7
  * e.g. 'asks the http client to get x'
8
8
  *
9
- * @TODO
10
- * - find clear naming policy
11
- * - use speaking event names, even though they are longer
12
- * - distinguish between a information and request (e.g. store_dispatch is a request to the store)
13
9
  */
14
- const legacyEventWhitelist = [
15
- "socket_joinRoom",
16
- "socket_leaveRoom",
17
- "socket_dataUpdated",
18
- "ui_setupProject",
19
- "ui_addToSnackBar",
20
- ]
21
- const eventWhitelist = legacyEventWhitelist.concat([
10
+
11
+ const eventWhitelist = [
22
12
  "forms/init",
23
13
  "forms/validate",
24
14
  "widgets/reset/workingState",
@@ -27,7 +17,8 @@ const eventWhitelist = legacyEventWhitelist.concat([
27
17
  "socket/data/update",
28
18
  "socket/join/room",
29
19
  "socket/leave/room",
30
- ])
20
+ "backend/error",
21
+ ]
31
22
  const eventWildcards = [
32
23
  "query_deleted",
33
24
  "store_actionResolved",
@@ -159,12 +159,18 @@ const generateLoginCallback =
159
159
  success: true,
160
160
  token: token,
161
161
  redirect: () => {
162
+ const params = new URLSearchParams(window.location.search)
163
+ const redirect = params.get("redirect") || ""
162
164
  // If we're in the same origin, we keep the history
163
165
  if (
164
166
  _callbackUrl.startsWith(window.location.origin) ||
165
167
  _callbackUrl.startsWith("/")
166
168
  ) {
167
- window.location.href = _callbackUrl
169
+ if (redirect.length && redirect.startsWith("/:")) {
170
+ window.location.href = _callbackUrl + redirect
171
+ } else {
172
+ window.location.reload()
173
+ }
168
174
  } else {
169
175
  // Otherwise do a full page reload to the callback URL
170
176
  window.location.replace(_callbackUrl)
@@ -52,7 +52,6 @@ const onSubmitLogin = async (loginPayload: {
52
52
  }) => {
53
53
  if (props.loginMethod) {
54
54
  const { success, message, redirect } = await props.loginMethod(loginPayload)
55
-
56
55
  if (success) {
57
56
  redirect?.()
58
57
  } else if (message) {
@@ -11,6 +11,7 @@
11
11
  :order="-10"
12
12
  v-if="availableInterfaces.indexOf('topBar') > -1"
13
13
  id="appAppbar"
14
+ ref="appAppbar"
14
15
  color="topbar-bg"
15
16
  density="compact"
16
17
  flat
@@ -38,7 +39,8 @@
38
39
 
39
40
  <img v-if="logoExists" class="tw:h-[40px]" :src="logoSrc" alt="Logo" />
40
41
  </div>
41
-
42
+ <v-spacer />
43
+ <AppBarMenu />
42
44
  <v-spacer />
43
45
 
44
46
  <DevIndicator v-if="isDevelopment" />
@@ -60,7 +62,7 @@
60
62
  />
61
63
 
62
64
  <!-- Main content -->
63
- <v-main class="tw:flex tw:flex-col">
65
+ <v-main class="tw:flex tw:flex-col" id="appMainContent">
64
66
  <div class="tw:w-full tw-h-1 tw:top-[49px]">
65
67
  <v-progress-linear
66
68
  :height="2"
@@ -80,6 +82,7 @@
80
82
  slot-type="page"
81
83
  :slot-layout="slotLayout"
82
84
  @scroll.passive="onScroll($event)"
85
+ :max-height="availableComponentHeight"
83
86
  />
84
87
 
85
88
  <v-empty-state
@@ -98,6 +101,7 @@
98
101
  <v-footer
99
102
  :order="-1"
100
103
  color="footer-bg"
104
+ ref="appFooter"
101
105
  border-thin
102
106
  v-if="configService.getPleaseLetUsAdvertiseOurselves()"
103
107
  class="tw:items-center tw:text-center tw:flex tw:flex-col tw:border-t-1 tw:grow-0! tw:mt-auto"
@@ -123,7 +127,15 @@
123
127
  </template>
124
128
 
125
129
  <script setup lang="ts">
126
- import { onMounted, ref, watch, computed, defineAsyncComponent } from "vue"
130
+ import {
131
+ onMounted,
132
+ onUnmounted,
133
+ useTemplateRef,
134
+ ref,
135
+ watch,
136
+ computed,
137
+ defineAsyncComponent,
138
+ } from "vue"
127
139
  import UserNotifications from "./components/menu/UserNotifications.vue"
128
140
  import PageNavigation from "./components/navigation/PageNavigationSidebar.vue"
129
141
  import snackbarStack from "./components/snackbar/SnackStack.vue"
@@ -142,6 +154,9 @@ import { isDevelopmentMode } from "@racletteCore/helpers/devMode"
142
154
  import useDragAndDrop from "./composables/useDragAndDrop"
143
155
  import { useCurrentComposition } from "./composables"
144
156
  import WidgetsLayoutLoader from "./components/composition/WidgetsLayoutLoader.vue"
157
+ import AppBarMenu from "./components/menu/AppBarMenu.vue"
158
+ import { useWindowSize } from "@vueuse/core"
159
+ const { height: windowHeight } = useWindowSize()
145
160
 
146
161
  useTitle(`raclette - ${configService.getApplicationTitle()}`)
147
162
  const route = useRoute()
@@ -154,6 +169,22 @@ useRouteState()
154
169
  const { isDraggingOrResizingAnything } = useDragAndDrop()
155
170
  const { widgetsLayout, slotLayout } = useCurrentComposition()
156
171
 
172
+ const appAppbarRef = useTemplateRef("appAppbar")
173
+ const appFooterRef = useTemplateRef("appFooter")
174
+ const availableComponentHeight = ref(0)
175
+
176
+ const updateAvailableHeight = () => {
177
+ const appbarHeight = appAppbarRef.value?.offsetHeight || 0
178
+ const appFooterHeight = appFooterRef.value?.offsetHeight || 0
179
+ const progressBarHeight = 2 // Progress bar height
180
+ const borderAndPadding = 8 // Border and padding buffer
181
+ availableComponentHeight.value =
182
+ windowHeight.value -
183
+ appbarHeight -
184
+ appFooterHeight -
185
+ progressBarHeight -
186
+ borderAndPadding
187
+ }
157
188
  const showWelcomeScreen = computed(() => {
158
189
  const isDynamicPage = route.name === "app.space.page"
159
190
  let hasPageIdParameter = Boolean(route.params.pageId?.length)
@@ -170,7 +201,7 @@ const WelcomeScreenComponent = defineAsyncComponent({
170
201
  template: `
171
202
  <div class="tw:flex tw:items-center tw:justify-center tw:h-full tw:text-center tw:p-8">
172
203
  <div>
173
- <h1 class="tw:text-4xl tw:font-bold tw:mb-4 tw:text-gray-800">Welcome to Raclette</h1>
204
+ <h1 class="tw:text-4xl tw:font-bold tw:mb-4 tw:text-gray-800">Welcome to racletteJS</h1>
174
205
  <p class="tw:text-gray-600 tw:text-lg">Get started by selecting a page from the navigation.</p>
175
206
  </div>
176
207
  </div>
@@ -232,8 +263,12 @@ onMounted(async () => {
232
263
  logoExists.value = await checkImageExists(logoSrc.value)
233
264
  // check if we have a message from checkToken to display as a snackbar
234
265
  loginUserNotifications()
266
+ updateAvailableHeight()
267
+ window.addEventListener("resize", updateAvailableHeight)
268
+ })
269
+ onUnmounted(() => {
270
+ window.removeEventListener("resize", updateAvailableHeight)
235
271
  })
236
-
237
272
  watch(theme, () => {
238
273
  setTheme(theme.value)
239
274
  })
@@ -1,11 +1,12 @@
1
1
  <template>
2
2
  <DefaultModal v-model="showModal">
3
- <div class="tw:p-5 tw:h-full">
3
+ <div class="tw:p-5 tw:h-full" ref="appCompositionOverlay">
4
4
  <WidgetsLayoutLoader
5
5
  v-if="widgetsLayout"
6
6
  :widgets-layout="widgetsLayout"
7
7
  :slot-layout="slotLayout"
8
8
  slot-type="modal"
9
+ :max-height="availableComponentHeight"
9
10
  />
10
11
  </div>
11
12
  </DefaultModal>
@@ -14,10 +15,13 @@
14
15
  <script setup lang="ts">
15
16
  import { DefaultModal, WidgetsLayoutLoader } from "@racletteOrchestrator"
16
17
  import useCurrentComposition from "@racletteOrchestrator/composables/useCurrentComposition"
17
- import { computed } from "vue"
18
+ import { computed, useTemplateRef } from "vue"
18
19
  import useRouteState from "@racletteOrchestrator/composables/useRouteState"
19
20
  import { DEFAULT_MODAL_SLOT_NAME } from "@racletteOrchestrator/router/routeParserHelper"
20
-
21
+ const appCompositionOverlayRef = useTemplateRef("appCompositionOverlay")
22
+ const availableComponentHeight = computed(
23
+ () => appCompositionOverlayRef?.value?.offsetHeight || 400,
24
+ )
21
25
  const { widgetsLayout, slotLayout } = useCurrentComposition(
22
26
  DEFAULT_MODAL_SLOT_NAME,
23
27
  )
@@ -59,7 +59,6 @@
59
59
  v-if="slot.widget"
60
60
  :is="getSlotWidget(slot.widget)"
61
61
  :uuid="slot.widget.uuid"
62
- class="tw:h-full"
63
62
  :class="[
64
63
  hasSpacing &&
65
64
  widgetStates[slot.widget.uuid].config
@@ -91,16 +90,20 @@ import type { WidgetBase } from "@racletteCore/store/types"
91
90
  import { watch, ref, h, computed, type Component } from "vue"
92
91
  import { widgetLoader } from "@racletteOrchestrator/helpers/widgetLoader"
93
92
  import { useDisplay } from "vuetify"
94
- import { useWindowSize } from "@vueuse/core"
95
93
  import useStateSubscriber from "@racletteOrchestrator/composables/useStateSubscriber"
96
94
  import useRouteState from "@racletteOrchestrator/composables/useRouteState"
97
95
 
98
- const props = defineProps<{
99
- widgetsLayout: Composition["widgetsLayout"]
100
- slotType: String
101
- slotLayout: String
102
- }>()
103
-
96
+ const props = withDefaults(
97
+ defineProps<{
98
+ widgetsLayout: Composition["widgetsLayout"]
99
+ slotType: string
100
+ slotLayout: string
101
+ maxHeight?: number
102
+ }>(),
103
+ {
104
+ maxHeight: 0,
105
+ },
106
+ )
104
107
  const { compositionSlots } = useRouteState()
105
108
 
106
109
  const currentRouteConfig = computed(() =>
@@ -131,7 +134,6 @@ const rowClasses = computed(() => {
131
134
  })
132
135
 
133
136
  const { mdAndDown } = useDisplay()
134
- const { height: windowHeight } = useWindowSize()
135
137
 
136
138
  const { state: widgetStates } = useStateSubscriber("widgets")
137
139
  const routeParams = computed(() => {
@@ -152,26 +154,12 @@ const routeParams = computed(() => {
152
154
  // Create a reactive key that changes when widgetsLayout changes
153
155
  const gridKey = ref(0)
154
156
 
155
- // Calculate available height for widgets
156
- const availableHeight = computed(() => {
157
- // this is really just a simplified approach for now
158
-
159
- // TODO: this needs to be more dynamic
160
- const appBarHeight = 48 // Vuetify compact app bar height
161
- const progressBarHeight = 2 // Progress bar height
162
- const borderAndPadding = 8 // Border and padding buffer
163
-
164
- const calculated =
165
- windowHeight.value - appBarHeight - progressBarHeight - borderAndPadding
166
- return calculated
167
- })
168
-
169
157
  // Calculate max height per widget based on grid layout
170
158
  const maxWidgetHeight = computed(() => {
171
159
  // Always set max height to the calculated available screen height (with buffer)
172
160
  // Later we should determine a max value based on other widgets in each row, so this value will be different across rows and columns
173
161
 
174
- return Math.min(availableHeight.value, 2400)
162
+ return Math.min(props.maxHeight, 2400)
175
163
  })
176
164
 
177
165
  const widgetComponentCache = new Map<string, Component>()
@@ -1,11 +1,11 @@
1
1
  <template>
2
2
  <v-text-field
3
3
  ref="search-input"
4
- v-model="model"
4
+ v-model="workingString"
5
5
  clearable
6
6
  :autofocus="autoFocus"
7
7
  density="compact"
8
- :label="$t('core.search_input')"
8
+ :label="label || $t('core.search_input')"
9
9
  prepend-inner-icon="mdi-magnify"
10
10
  flat
11
11
  hide-details
@@ -14,12 +14,15 @@
14
14
  </template>
15
15
 
16
16
  <script setup lang="ts">
17
- import { useTemplateRef, onMounted } from "vue"
17
+ import { useTemplateRef, onMounted, computed } from "vue"
18
18
  import { awaitTime } from "@racletteCore/lib/dataHelper"
19
+ import { debounceHelper } from "@raclettejs/core/frontend"
20
+ const debounce = debounceHelper()
19
21
 
20
22
  const props = withDefaults(
21
23
  defineProps<{
22
24
  autoFocus?: boolean
25
+ label?: string
23
26
  }>(),
24
27
  {
25
28
  autoFocus: false,
@@ -30,7 +33,16 @@ const model = defineModel({
30
33
  type: String,
31
34
  required: true,
32
35
  })
33
-
36
+ const workingString = computed({
37
+ get() {
38
+ return model.value
39
+ },
40
+ set(newVal) {
41
+ debounce(() => {
42
+ model.value = newVal
43
+ })
44
+ },
45
+ })
34
46
  const input = useTemplateRef("search-input")
35
47
 
36
48
  onMounted(async () => {
@@ -0,0 +1,184 @@
1
+ <template>
2
+ <div class="tw:h-full tw:flex tw-items-stretchc" ref="appAppbarMenu">
3
+ <template v-for="item in pageNavigation.appBarItems" :key="item.route">
4
+ <v-tab
5
+ :key="locale"
6
+ v-if="item.slotType !== 'inline'"
7
+ :disabled="activeRouteName === item.route || !item.route"
8
+ :active="activeRouteName === item.route"
9
+ :value="item.route || ''"
10
+ color="primary"
11
+ class="nav-link tw:h-full tw-flex tw-items-center tw-justify-center"
12
+ link
13
+ @click="navigate(item)"
14
+ :prepend-icon="item.icon"
15
+ :text="item.title"
16
+ />
17
+ <template v-else>
18
+ <template v-if="item.icon || item.title">
19
+ <v-tab
20
+ class="tw:h-full tw-flex tw-items-stretch"
21
+ :color="floatingWindow.visible ? 'primary' : 'none'"
22
+ @click="openFloatingWindow(item)"
23
+ ><v-icon>{{ item.icon || "mdi-window-restore" }}</v-icon
24
+ >{{ item.title }}</v-tab
25
+ >
26
+ </template>
27
+ <div v-else class="tw:h-full tw-flex tw-items-stretch">
28
+ <WidgetsLayoutLoader
29
+ v-if="compositions[item.compositionId]"
30
+ :widgets-layout="compositions[item.compositionId].widgetsLayout"
31
+ slot-type="inline"
32
+ :slot-layout="compositions[item.compositionId].slotLayout"
33
+ :max-height="availableComponentHeight"
34
+ />
35
+ </div>
36
+ </template>
37
+ </template>
38
+
39
+ <!-- Floating Window -->
40
+ <teleport to="body">
41
+ <div
42
+ v-if="floatingWindow.visible"
43
+ :style="{
44
+ position: 'fixed',
45
+ left: floatingWindow.x + 'px',
46
+ top: floatingWindow.y + 'px',
47
+ zIndex: 9999,
48
+ cursor: isDragging ? 'grabbing' : 'grab',
49
+ }"
50
+ @mousedown="startDrag"
51
+ class="tw:select-none"
52
+ >
53
+ <v-card min-width="400" max-width="800" elevation="8">
54
+ <v-card-title
55
+ class="d-flex justify-space-between align-center bg-primary"
56
+ >
57
+ <span>{{ floatingWindow.item?.title || "Widget" }}</span>
58
+ <v-btn
59
+ icon="mdi-close"
60
+ size="small"
61
+ variant="text"
62
+ @click.stop="closeFloatingWindow"
63
+ ></v-btn>
64
+ </v-card-title>
65
+ <v-card-text class="pa-4">
66
+ <WidgetsLayoutLoader
67
+ v-if="
68
+ floatingWindow.item &&
69
+ compositions[floatingWindow.item.compositionId]
70
+ "
71
+ :max-height="800"
72
+ :widgets-layout="
73
+ compositions[floatingWindow.item.compositionId].widgetsLayout
74
+ "
75
+ slot-type="inline"
76
+ :slot-layout="
77
+ compositions[floatingWindow.item.compositionId].slotLayout
78
+ "
79
+ />
80
+ </v-card-text>
81
+ </v-card>
82
+ </div>
83
+ </teleport>
84
+ </div>
85
+ </template>
86
+
87
+ <script setup lang="ts">
88
+ import { computed, useTemplateRef, reactive, ref } from "vue"
89
+ import { useRouter } from "vue-router"
90
+ import useRouteState from "@racletteOrchestrator/composables/useRouteState"
91
+ import usePageNavigation, {
92
+ type NavigationItem,
93
+ } from "@racletteOrchestrator/composables/usePageNavigation"
94
+ import WidgetsLayoutLoader from "../composition/WidgetsLayoutLoader.vue"
95
+ import { DEFAULT_SLOT_NAME } from "@racletteOrchestrator/router/routeParserHelper"
96
+ import useStateSubscriber from "@racletteOrchestrator/composables/useStateSubscriber"
97
+
98
+ const appAppbarMenuRef = useTemplateRef("appAppbarMenu")
99
+ const availableComponentHeight = computed(
100
+ () => appAppbarMenuRef?.value?.offsetHeight || 48,
101
+ )
102
+ const { state: pageNavigation } = usePageNavigation()
103
+ const { state: compositions } = useStateSubscriber("compositions")
104
+ const router = useRouter()
105
+ const { getCompositionLinkForSlotName } = useRouteState()
106
+ const currentPageRoute = getCompositionLinkForSlotName(DEFAULT_SLOT_NAME)
107
+
108
+ const activeRouteName = computed(() => {
109
+ const currentRoute = router.currentRoute.value
110
+ const pageCompositionLinkValue = currentPageRoute?.value?.slotValue
111
+
112
+ if (currentRoute.name === "app.space.page") {
113
+ return pageCompositionLinkValue || currentRoute.params.pageId
114
+ }
115
+
116
+ return pageCompositionLinkValue || currentRoute.name
117
+ })
118
+
119
+ // Floating window state
120
+ const floatingWindow = reactive({
121
+ visible: false,
122
+ x: 100,
123
+ y: 100,
124
+ item: null as NavigationItem | null,
125
+ })
126
+
127
+ const isDragging = ref(false)
128
+ const dragOffset = reactive({ x: 0, y: 0 })
129
+
130
+ const openFloatingWindow = (item: NavigationItem) => {
131
+ if (floatingWindow.item) return closeFloatingWindow()
132
+ floatingWindow.item = item
133
+ floatingWindow.visible = true
134
+ // Center the window on screen
135
+ floatingWindow.x = (window.innerWidth - 400) / 2
136
+ floatingWindow.y = (window.innerHeight - 300) / 2
137
+ }
138
+
139
+ const closeFloatingWindow = () => {
140
+ floatingWindow.visible = false
141
+ floatingWindow.item = null
142
+ }
143
+
144
+ const startDrag = (e: MouseEvent) => {
145
+ // Don't drag if clicking on close button or other interactive elements
146
+ if ((e.target as HTMLElement).closest(".v-btn")) {
147
+ return
148
+ }
149
+
150
+ isDragging.value = true
151
+ dragOffset.x = e.clientX - floatingWindow.x
152
+ dragOffset.y = e.clientY - floatingWindow.y
153
+
154
+ let rafPending = false
155
+ let lastEvent: MouseEvent
156
+
157
+ const onMouseMove = (e: MouseEvent) => {
158
+ lastEvent = e
159
+ if (!rafPending) {
160
+ rafPending = true
161
+ requestAnimationFrame(() => {
162
+ rafPending = false
163
+ if (isDragging.value) {
164
+ floatingWindow.x = lastEvent.clientX - dragOffset.x
165
+ floatingWindow.y = lastEvent.clientY - dragOffset.y
166
+ }
167
+ })
168
+ }
169
+ }
170
+
171
+ const onMouseUp = () => {
172
+ isDragging.value = false
173
+ document.removeEventListener("mousemove", onMouseMove)
174
+ document.removeEventListener("mouseup", onMouseUp)
175
+ }
176
+
177
+ document.addEventListener("mousemove", onMouseMove)
178
+ document.addEventListener("mouseup", onMouseUp)
179
+ }
180
+
181
+ const navigate = (item: NavigationItem) => {
182
+ item.navigateTo()
183
+ }
184
+ </script>