@raclettejs/core 0.1.23 → 0.1.24

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 (36) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/bin/cli.js +2 -2
  3. package/dist/cli.js +136 -155
  4. package/dist/cli.js.map +4 -4
  5. package/dist/index.js +15 -16
  6. package/dist/index.js.map +4 -4
  7. package/package.json +24 -22
  8. package/services/backend/.yarn/install-state.gz +0 -0
  9. package/services/backend/package.json +21 -19
  10. package/services/backend/src/core/contracting/contractRegistrar.ts +1 -1
  11. package/services/backend/src/core/pluginSystem/configGenerator/generatorTypes.ts +1 -0
  12. package/services/backend/src/core/pluginSystem/configGenerator/index.ts +2 -2
  13. package/services/backend/src/core/pluginSystem/pluginLoader.ts +99 -44
  14. package/services/backend/src/core/pluginSystem/pluginTypes.ts +1 -0
  15. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/index.ts +2 -0
  16. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patchOwn.ts +68 -0
  17. package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/user.model.ts +1 -1
  18. package/services/backend/tsconfig.json +1 -0
  19. package/services/backend/yarn.lock +698 -1061
  20. package/services/frontend/.yarn/install-state.gz +0 -0
  21. package/services/frontend/eslint.config.js +9 -0
  22. package/services/frontend/index.html +3 -3
  23. package/services/frontend/package.json +30 -28
  24. package/services/frontend/src/core/setup/plugin-system/discovery/app-plugins.ts +43 -4
  25. package/services/frontend/src/orchestrator/ProductOrchestrator.vue +10 -11
  26. package/services/frontend/src/orchestrator/assets/styles/layers.css +11 -0
  27. package/services/frontend/src/orchestrator/assets/styles/tailwindStyles.css +5 -11
  28. package/services/frontend/src/orchestrator/assets/styles/vuetifyStyles.scss +0 -4
  29. package/services/frontend/src/orchestrator/components/composition/WidgetsLayoutLoader.vue +5 -18
  30. package/services/frontend/src/orchestrator/composables/useWidgets/helperFunctions.ts +54 -5
  31. package/services/frontend/src/orchestrator/helpers/index.ts +1 -0
  32. package/services/frontend/src/orchestrator/helpers/widgetVisuals.ts +71 -0
  33. package/services/frontend/src/orchestrator/setup/vuetify.ts +2 -0
  34. package/services/frontend/src/orchestrator/types/Widgets.ts +5 -1
  35. package/services/frontend/vite.config.ts +22 -9
  36. package/services/frontend/yarn.lock +0 -5224
@@ -6,6 +6,15 @@ import globals from "globals"
6
6
  export default [
7
7
  ...parentConfig,
8
8
 
9
+ {
10
+ files: ["**/*.{ts,tsx}"],
11
+ languageOptions: {
12
+ parserOptions: {
13
+ project: "./tsconfig.app.json",
14
+ },
15
+ },
16
+ },
17
+
9
18
  // Vue config
10
19
  ...pluginVue.configs["flat/recommended"],
11
20
  {
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html class="tw:overscroll-none">
2
+ <html class="tw:h-full tw:overscroll-none tw:overflow-hidden">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <link rel="icon" href="/favicon.png" />
@@ -11,8 +11,8 @@
11
11
  </script>
12
12
  </head>
13
13
 
14
- <body class="tw:overscroll-none">
15
- <div id="app"></div>
14
+ <body class="tw:h-full tw:overscroll-none tw:overflow-hidden">
15
+ <div id="app" class="tw:h-full"></div>
16
16
  <script type="module" src="/src/orchestrator/index.ts"></script>
17
17
  </body>
18
18
  </html>
@@ -18,44 +18,46 @@
18
18
  "dependencies": {
19
19
  "@date-io/date-fns": "3.2.1",
20
20
  "@mdi/font": "7.4.47",
21
- "@sinclair/typebox": "0.34.41",
22
- "@tailwindcss/vite": "4.1.13",
23
- "@tsconfig/node20": "20.1.6",
24
- "@vueuse/core": "14.1.0",
25
- "@vueuse/rxjs": "14.1.0",
26
- "axios": "1.13.2",
21
+ "@sinclair/typebox": "0.34.48",
22
+ "@tailwindcss/vite": "4.2.2",
23
+ "@tsconfig/node20": "20.1.9",
24
+ "@vueuse/core": "14.2.1",
25
+ "@vueuse/rxjs": "14.2.1",
26
+ "axios": "1.13.6",
27
27
  "date-fns": "4.1.0",
28
- "esm": "3.x",
28
+ "esm": "^3.2.25",
29
29
  "interactjs": "1.10.27",
30
30
  "mini-rx-store": "6.1.2",
31
- "nanoid": "5.1.5",
32
- "ramda": "0.31.3",
33
- "rxjs": "7.x",
34
- "sass": "1.92.1",
35
- "socket.io-client": "4.x",
36
- "tailwindcss": "4.1.13",
31
+ "nanoid": "5.1.7",
32
+ "ramda": "0.32.0",
33
+ "rxjs": "7.8.2",
34
+ "sass": "1.98.0",
35
+ "socket.io-client": "^4.8.3",
36
+ "tailwindcss": "4.2.2",
37
37
  "uuid": "13.0.0",
38
- "vue": "3.5.25",
39
- "vue-i18n": "11.2.2",
38
+ "vue": "3.5.30",
39
+ "vue-i18n": "11.3.0",
40
40
  "vue-router": "4.6.3",
41
- "vuetify": "3.11.2"
41
+ "vuetify": "4.0.5"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/node": "24.5.1",
45
45
  "@types/ramda": "0.31.1",
46
- "@vitejs/plugin-vue": "6.0.1",
47
- "@vue/tsconfig": "0.8.1",
46
+ "@vitejs/plugin-vue": "6.0.5",
47
+ "@vue/tsconfig": "0.9.0",
48
48
  "eslint": "9.35.0",
49
- "eslint-config-prettier": "10.1.8",
49
+ "@eslint/js": "9.35.0",
50
+ "eslint-config-prettier": "9.1.2",
50
51
  "eslint-plugin-import": "2.32.0",
51
- "eslint-plugin-prefer-arrow-functions": "3.8.1",
52
- "eslint-plugin-tailwindcss": "4.0.0-beta.0",
53
- "eslint-plugin-vue": "10.4.0",
54
- "globals": "16.4.0",
55
- "prettier": "3.6.2",
56
- "typescript": "5.9.2",
52
+ "eslint-plugin-prefer-arrow-functions": "3.9.1",
53
+ "eslint-plugin-tailwindcss": "3.18.2",
54
+ "eslint-plugin-vue": "9.33.0",
55
+ "globals": "17.4.0",
56
+ "prettier": "3.8.1",
57
+ "typescript": "5.9.3",
57
58
  "vite": "7.1.5",
58
- "vue-eslint-parser": "10.2.0",
59
- "vue-tsc": "3.0.7"
59
+ "vite-plugin-vuetify": "2.1.3",
60
+ "vue-eslint-parser": "9.4.3",
61
+ "vue-tsc": "3.2.6"
60
62
  }
61
- }
63
+ }
@@ -3,6 +3,45 @@ import logger from "@racletteCore/lib/logger"
3
3
  import { validatePluginMetadata } from "../utils/operations"
4
4
  import type { DiscoveredPlugin, PluginType } from "../types"
5
5
 
6
+ /** Keys are known at build time; covers both `…/frontend/` and `…/src/frontend/`. */
7
+ const appPluginFrontendIndexKeys = new Set(
8
+ Object.keys(
9
+ import.meta.glob(
10
+ [
11
+ "/src/plugins/**/frontend/index.ts",
12
+ "/src/workbenchPlugins/**/frontend/index.ts",
13
+ ],
14
+ { eager: false },
15
+ ),
16
+ ),
17
+ )
18
+
19
+ function resolveAppPluginFrontendPath(
20
+ pluginPath: string,
21
+ metadata: { frontendDir?: string },
22
+ ): string {
23
+ const explicit = metadata.frontendDir?.replace(/^\.\//, "")
24
+ if (explicit) {
25
+ return `${pluginPath}/${explicit}`
26
+ }
27
+
28
+ const direct = `${pluginPath}/frontend`
29
+ const underSrc = `${pluginPath}/src/frontend`
30
+ const directIndex = `${direct}/index.ts`
31
+ const srcIndex = `${underSrc}/index.ts`
32
+
33
+ const hasDirect = appPluginFrontendIndexKeys.has(directIndex)
34
+ const hasUnderSrc = appPluginFrontendIndexKeys.has(srcIndex)
35
+
36
+ if (hasDirect && hasUnderSrc) {
37
+ return direct
38
+ }
39
+ if (hasUnderSrc) {
40
+ return underSrc
41
+ }
42
+ return direct
43
+ }
44
+
6
45
  /**
7
46
  * Discover app/user plugins using static glob pattern with exclusions
8
47
  */
@@ -22,7 +61,9 @@ export const discoverAppPlugins = async (): Promise<DiscoveredPlugin[]> => {
22
61
 
23
62
  for (const configPath of Object.keys(configModules)) {
24
63
  try {
25
- const configModule = await configModules[configPath]()
64
+ const loadConfig = configModules[configPath]
65
+ if (!loadConfig) continue
66
+ const configModule = await loadConfig()
26
67
  const metadata = (configModule as any)?.default || configModule
27
68
 
28
69
  if (!validatePluginMetadata(metadata)) {
@@ -31,9 +72,7 @@ export const discoverAppPlugins = async (): Promise<DiscoveredPlugin[]> => {
31
72
  }
32
73
 
33
74
  const pluginPath = configPath.replace(/\/raclette\.plugin\.ts$/, "")
34
- const frontendRelativePath =
35
- metadata.frontendDir?.replace(/^\.\//, "") || "frontend"
36
- const frontendPath = `${pluginPath}/${frontendRelativePath}`
75
+ const frontendPath = resolveAppPluginFrontendPath(pluginPath, metadata)
37
76
 
38
77
  const pluginKey = generatePluginKey(metadata.name, metadata.author)
39
78
 
@@ -62,7 +62,10 @@
62
62
  />
63
63
 
64
64
  <!-- Main content -->
65
- <v-main class="tw:flex tw:flex-col" id="appMainContent">
65
+ <v-main
66
+ class="tw:flex tw:flex-col tw:min-h-0 tw:overflow-y-auto tw:h-screen"
67
+ id="appMainContent"
68
+ >
66
69
  <div class="tw:w-full tw-h-1 tw:top-[49px]">
67
70
  <v-progress-linear
68
71
  :height="2"
@@ -76,13 +79,11 @@
76
79
  <template v-else>
77
80
  <WidgetsLayoutLoader
78
81
  v-if="widgetsLayout"
79
- class="tw:overflow-y-auto! tw:pb-0"
82
+ class="tw:min-h-0 tw:pb-0"
80
83
  :class="{ '!tw:invisible': loading }"
81
84
  :widgets-layout="widgetsLayout"
82
85
  slot-type="page"
83
86
  :slot-layout="slotLayout"
84
- @scroll.passive="onScroll($event)"
85
- :max-height="availableComponentHeight"
86
87
  />
87
88
 
88
89
  <v-empty-state
@@ -104,7 +105,7 @@
104
105
  ref="appFooter"
105
106
  border-thin
106
107
  v-if="configService.getPleaseLetUsAdvertiseOurselves()"
107
- class="tw:items-center tw:text-center tw:flex tw:flex-col tw:border-t-1 tw:grow-0! tw:mt-auto"
108
+ class="tw:items-center tw:text-center tw:flex tw:flex-col tw:border-t tw:grow-0! tw:mt-auto"
108
109
  >
109
110
  <a
110
111
  href="https://www.raclettejs.com"
@@ -174,8 +175,10 @@ const appFooterRef = useTemplateRef("appFooter")
174
175
  const availableComponentHeight = ref(0)
175
176
 
176
177
  const updateAvailableHeight = () => {
177
- const appbarHeight = appAppbarRef.value?.offsetHeight || 0
178
- const appFooterHeight = appFooterRef.value?.offsetHeight || 0
178
+ const appbarHeight =
179
+ (appAppbarRef.value?.$el as HTMLElement | undefined)?.offsetHeight || 0
180
+ const appFooterHeight =
181
+ (appFooterRef.value?.$el as HTMLElement | undefined)?.offsetHeight || 0
179
182
  const progressBarHeight = 2 // Progress bar height
180
183
  const borderAndPadding = 8 // Border and padding buffer
181
184
  availableComponentHeight.value =
@@ -239,7 +242,6 @@ const checkImageExists = async (url: string): Promise<boolean> => {
239
242
  }
240
243
  }
241
244
 
242
- const showScrollToTopBtn = ref(false)
243
245
  const isDevelopment = ref(false)
244
246
 
245
247
  const setTheme = (theme: "dark" | "light") => {
@@ -253,9 +255,6 @@ const setTheme = (theme: "dark" | "light") => {
253
255
  }
254
256
  }
255
257
 
256
- const onScroll = (e) =>
257
- (showScrollToTopBtn.value = e.currentTarget.scrollTop > 0)
258
-
259
258
  onMounted(async () => {
260
259
  setTheme(theme.value)
261
260
  vuetifyLocale.current.value = locale.value
@@ -0,0 +1,11 @@
1
+ @layer tailwind-theme;
2
+ @layer tailwind-reset;
3
+
4
+ @layer vuetify-core;
5
+ @layer vuetify-components;
6
+ @layer vuetify-overrides;
7
+ @layer vuetify-utilities;
8
+
9
+ @layer tailwind-utilities;
10
+
11
+ @layer vuetify-final;
@@ -1,18 +1,12 @@
1
1
  @source "../../../../index.html";
2
2
  @source "/**/*.{vue,js,ts,jsx,tsx,html}";
3
- /* vite aliase? */
4
- /* @source "@racletteOrchestrator" */
5
3
 
6
- @import "tailwindcss" prefix(tw);
4
+ @import "tailwindcss/theme" layer(tailwind-theme) prefix(tw);
5
+ @import "tailwindcss/preflight" layer(tailwind-reset) prefix(tw);
6
+ @import "tailwindcss/utilities" layer(tailwind-utilities) prefix(tw);
7
7
 
8
- /* The commented code below allows to granually import tailwind */
9
- /* @layer theme, base, components, utilities; */
10
- /* @import "tailwindcss/theme.css" layer(theme) prefix(tw); */
11
- /* @import "tailwindcss/preflight.css" layer(base) prefix(tw); */
12
- /* @import "tailwindcss/utilities.css" layer(utilities) prefix(tw); */
13
-
14
- /* enable dark mode via class */
15
- @custom-variant dark (&:where(.dark, .dark *));
8
+ @custom-variant light (&:where(.v-theme--light, .v-theme--light *));
9
+ @custom-variant dark (&:where(.v-theme--dark, .v-theme--dark *));
16
10
 
17
11
  /* safelist for the resizing in the composition editor */
18
12
  @source inline('tw:col-span-{1..12}');
@@ -1,7 +1,3 @@
1
- @use "vuetify" with (
2
- $utilities: false,
3
- $reset: false
4
- );
5
1
  @use "@mdi/font/css/materialdesignicons.css";
6
2
 
7
3
  // The styles below are picked from vuetify's reset css (which is disabled)
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="tw:h-full tw:max-w-full tw:bg-gray-50 tw:dark:bg-[#252525]"
3
+ class="tw:max-w-full tw:bg-gray-50 tw:dark:bg-[#252525] tw:min-h-fit"
4
4
  :class="{
5
5
  'tw:p-4':
6
6
  slotLayout !== 'default' &&
@@ -30,7 +30,7 @@
30
30
  >
31
31
  <v-row
32
32
  :no-gutters="slotLayout === 'default' || slotLayout === 'full'"
33
- :class="{ 'tw:h-full': slotLayout === 'full', ...rowClasses }"
33
+ :class="[{ 'tw:h-full': slotLayout === 'full' }, rowClasses]"
34
34
  >
35
35
  <TransitionGroup
36
36
  name="grid-item"
@@ -44,17 +44,12 @@
44
44
  :cols="getSlotCols(slot)"
45
45
  :style="{
46
46
  'transition-delay': `${slotIndex * 50}ms`,
47
- 'max-height': `${maxWidgetHeight}px`,
48
47
  ...(slotLayout === 'default' || slotLayout === 'full'
49
48
  ? { padding: '0px' }
50
49
  : {}),
51
50
  }"
52
51
  >
53
- <div
54
- :class="widgetContainerClasses"
55
- class="tw:h-full"
56
- :style="{ 'max-height': `${maxWidgetHeight}px` }"
57
- >
52
+ <div :class="widgetContainerClasses" class="tw:h-full">
58
53
  <component
59
54
  v-if="slot.widget"
60
55
  :is="getSlotWidget(slot.widget)"
@@ -108,11 +103,8 @@ const props = withDefaults(
108
103
  widgetsLayout: Composition["widgetsLayout"]
109
104
  slotType: string
110
105
  slotLayout: string
111
- maxHeight?: number
112
106
  }>(),
113
- {
114
- maxHeight: 0,
115
- },
107
+ {},
116
108
  )
117
109
 
118
110
  const { compositionSlots } = useRouteState()
@@ -122,7 +114,7 @@ const currentRouteConfig = computed(() =>
122
114
  )
123
115
 
124
116
  const widgetContainerClasses = computed(() => {
125
- const baseClasses = "tw:h-auto tw:overflow-auto"
117
+ const baseClasses = "tw:h-auto tw:overflow-visible"
126
118
 
127
119
  switch (props.slotLayout) {
128
120
  case "spaced":
@@ -165,11 +157,6 @@ const routeParams = computed(() => {
165
157
  // Create a reactive key that changes when widgetsLayout changes
166
158
  const gridKey = ref(0)
167
159
 
168
- // Calculate max height per widget based on grid layout
169
- const maxWidgetHeight = computed(() => {
170
- return Math.min(props.maxHeight, 2400)
171
- })
172
-
173
160
  // Calculate total number of widgets
174
161
  const totalWidgets = computed(() => {
175
162
  return props.widgetsLayout.reduce((sum, column) => sum + column.length, 0)
@@ -5,7 +5,9 @@
5
5
  import { forEachObjIndexed } from "ramda"
6
6
  import type { ConfigurableParameters, WidgetConf } from "@racletteOrchestrator"
7
7
  import { generatePluginKey } from "@shared/helper/pluginHelper"
8
- import type { PluginMetadata, RegisteredWidgetDeclaration } from "@core"
8
+ import type { PluginMetadata } from "@core"
9
+ import type { RegisteredWidgetDeclaration, WidgetDeclaration } from "@racletteOrchestrator"
10
+ import { normalizeWidgetVisuals } from "@racletteOrchestrator/helpers"
9
11
 
10
12
  // Plugin metadata files (unchanged)
11
13
  const RACLETTE_PLUGIN_FILES = import.meta.glob<{
@@ -14,6 +16,43 @@ const RACLETTE_PLUGIN_FILES = import.meta.glob<{
14
16
  eager: true,
15
17
  })
16
18
 
19
+ const EXTERNAL_PLUGIN_FRONTEND_INDEX_KEYS = new Set(
20
+ Object.keys(
21
+ import.meta.glob(
22
+ "@racletteCore/../../external-app-plugins/**/frontend/index.ts",
23
+ {
24
+ eager: false,
25
+ },
26
+ ),
27
+ ),
28
+ )
29
+
30
+ function resolveExternalPluginFrontendPath(
31
+ pluginDir: string,
32
+ metadata: PluginMetadata,
33
+ ): string {
34
+ const explicit = metadata.frontendDir?.replace(/^\.\//, "")
35
+ if (explicit) {
36
+ return `${pluginDir}/${explicit}`
37
+ }
38
+
39
+ const direct = `${pluginDir}/frontend`
40
+ const underSrc = `${pluginDir}/src/frontend`
41
+ const directIndex = `${direct}/index.ts`
42
+ const srcIndex = `${underSrc}/index.ts`
43
+
44
+ const hasDirect = EXTERNAL_PLUGIN_FRONTEND_INDEX_KEYS.has(directIndex)
45
+ const hasUnderSrc = EXTERNAL_PLUGIN_FRONTEND_INDEX_KEYS.has(srcIndex)
46
+
47
+ if (hasDirect && hasUnderSrc) {
48
+ return direct
49
+ }
50
+ if (hasUnderSrc) {
51
+ return underSrc
52
+ }
53
+ return direct
54
+ }
55
+
17
56
  // Discover widget Vue files to get widget names
18
57
  const WIDGET_COMPONENT_FILES = import.meta.glob(
19
58
  [
@@ -63,7 +102,11 @@ export const getWidgetDeclacations = () => {
63
102
 
64
103
  // Find all widget component files for this plugin
65
104
  const pluginDir = pluginPath.replace("/raclette.plugin.ts", "")
66
- const widgetPattern = `${pluginDir}/frontend/widgets/`
105
+ const frontendPath = resolveExternalPluginFrontendPath(
106
+ pluginDir,
107
+ pluginMetadata,
108
+ )
109
+ const widgetPattern = `${frontendPath}/widgets/`
67
110
 
68
111
  // Look through all discovered widget Vue files to find ones belonging to this plugin
69
112
  Object.keys(WIDGET_COMPONENT_FILES).forEach((componentPath) => {
@@ -91,7 +134,9 @@ export const getWidgetDeclacations = () => {
91
134
  }
92
135
 
93
136
  // Extract details and config (handle both default export and named exports)
94
- const details = setupModule.details || setupModule.default?.details
137
+ const details = (setupModule.details || setupModule.default?.details) as
138
+ | WidgetDeclaration
139
+ | undefined
95
140
  const config = setupModule.config || setupModule.default?.config
96
141
 
97
142
  if (!details) {
@@ -101,8 +146,10 @@ export const getWidgetDeclacations = () => {
101
146
  return
102
147
  }
103
148
 
149
+ const normalizedVisuals = normalizeWidgetVisuals(details, widgetName)
104
150
  const registeredWidget: RegisteredWidgetDeclaration = {
105
151
  ...details,
152
+ ...normalizedVisuals,
106
153
  pluginKey,
107
154
  author: pluginMetadata.author,
108
155
  name: widgetName,
@@ -169,11 +216,13 @@ const findSetupPathForWidget = (
169
216
  return null
170
217
  }
171
218
 
172
- const [pluginPath] = pluginEntry
219
+ const [pluginPath, pluginModule] = pluginEntry
173
220
  const pluginDir = pluginPath.replace("/raclette.plugin.ts", "")
221
+ const metadata = pluginModule.default
222
+ const frontendPath = resolveExternalPluginFrontendPath(pluginDir, metadata)
174
223
 
175
224
  // Look for a Vue file with this widget name to find the directory structure
176
- const widgetPattern = `${pluginDir}/frontend/widgets/`
225
+ const widgetPattern = `${frontendPath}/widgets/`
177
226
  const widgetVueFile = Object.keys(WIDGET_COMPONENT_FILES).find(
178
227
  (path) =>
179
228
  path.includes(widgetPattern) && path.endsWith(`${widgetName}Widget.vue`),
@@ -1,2 +1,3 @@
1
1
  export * from "./loginUserNotifications"
2
2
  export * from "./widgetLoader"
3
+ export * from "./widgetVisuals"
@@ -0,0 +1,71 @@
1
+ export type WidgetVisualInput = {
2
+ title?: string
3
+ color?: string
4
+ icon?: string
5
+ images?: string[]
6
+ }
7
+
8
+ export type WidgetVisuals = {
9
+ color: string
10
+ icon: string
11
+ images: string[]
12
+ }
13
+
14
+ const getHashValue = (value: string) =>
15
+ value.split("").reduce((acc, char) => acc * 31 + char.charCodeAt(0), 7)
16
+
17
+ export const generateColorFromName = (name: string): string => {
18
+ // Same input name -> same output color (stable across reloads/environments).
19
+ const normalizedName = name.trim().toLowerCase() || "widget"
20
+ const hash = Math.abs(getHashValue(normalizedName))
21
+ // Spread widgets over the full color wheel.
22
+ const hue = hash % 360
23
+ // Keep saturation/lightness in a readable mid-range to avoid too grey or too neon colors.
24
+ const saturation = 55 + (hash % 18) // 55..72
25
+ const lightness = 42 + (hash % 16) // 42..57
26
+ return `hsl(${hue} ${saturation}% ${lightness}%)`
27
+ }
28
+
29
+ export const getWidgetInitials = (name: string): string => {
30
+ const parts = name
31
+ .trim()
32
+ .split(/\s+/)
33
+ .filter(Boolean)
34
+
35
+ if (parts.length === 0) {
36
+ return "W"
37
+ }
38
+
39
+ if (parts.length === 1) {
40
+ return parts[0].slice(0, 2).toUpperCase()
41
+ }
42
+
43
+ return `${parts[0][0]}${parts[1][0]}`.toUpperCase()
44
+ }
45
+
46
+ export const normalizeWidgetVisuals = (
47
+ details: WidgetVisualInput,
48
+ widgetName: string,
49
+ ): WidgetVisuals => {
50
+ // Prefer plugin-provided values; only fallback when field is missing/empty.
51
+ const normalizedColor =
52
+ typeof details.color === "string" && details.color.trim().length > 0
53
+ ? details.color
54
+ : generateColorFromName(details.title || widgetName)
55
+
56
+ const normalizedIcon =
57
+ typeof details.icon === "string" && details.icon.trim().length > 0
58
+ ? details.icon
59
+ : ""
60
+
61
+ const normalizedImages = Array.isArray(details.images)
62
+ // Ignore invalid/empty entries to keep consumers simple.
63
+ ? details.images.filter((image) => typeof image === "string" && image.trim())
64
+ : []
65
+
66
+ return {
67
+ color: normalizedColor,
68
+ icon: normalizedIcon,
69
+ images: normalizedImages,
70
+ }
71
+ }
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Set up vuetify instance
3
3
  */
4
+ import "../assets/styles/layers.css"
5
+ import "vuetify/styles"
4
6
  import { createVuetify } from "vuetify"
5
7
  import * as components from "vuetify/components"
6
8
  import * as directives from "vuetify/directives"
@@ -53,4 +53,8 @@ export type RegisteredWidgetDeclaration = {
53
53
  export type WidgetDeclaration = Omit<
54
54
  RegisteredWidgetDeclaration,
55
55
  "pluginKey" | "author" | "name" // name will be extracted via filename, cannot be overwritten
56
- >
56
+ > & {
57
+ color?: string
58
+ icon?: string
59
+ images?: string[]
60
+ }
@@ -3,6 +3,7 @@ import vue from "@vitejs/plugin-vue"
3
3
  import path from "path"
4
4
  import { componentPathPlugin } from "./vite-plugin-component-path"
5
5
  import tailwindcss from "@tailwindcss/vite"
6
+ import Vuetify, { transformAssetUrls } from "vite-plugin-vuetify"
6
7
 
7
8
  export default defineConfig({
8
9
  server: {
@@ -17,16 +18,19 @@ export default defineConfig({
17
18
  target: "es2023",
18
19
  rollupOptions: {
19
20
  output: {
20
- manualChunks: {
21
- "vue-vendor": ["vue", "vue-router"],
22
- vuetify: ["vuetify"],
23
- // login: [
24
- // "@racletteOrchestrator/LoginApp.vue",
25
- // "@racletteOrchestrator/setup/loginApp.ts",
26
- // ],
21
+ manualChunks: (id) => {
22
+ if (id.includes("node_modules/vuetify")) {
23
+ return "vuetify"
24
+ }
25
+ if (
26
+ id.includes("node_modules/vue") ||
27
+ id.includes("node_modules/vue-router")
28
+ ) {
29
+ return "vue-vendor"
30
+ }
31
+ return undefined
27
32
  },
28
33
  },
29
- maxParallelFileOps: 1,
30
34
  },
31
35
  },
32
36
  optimizeDeps: {
@@ -37,7 +41,16 @@ export default defineConfig({
37
41
  "@racletteOrchestrator/composables/usePluginApi",
38
42
  ],
39
43
  },
40
- plugins: [vue(), componentPathPlugin(), tailwindcss()],
44
+ plugins: [
45
+ vue({
46
+ template: { transformAssetUrls },
47
+ }),
48
+ componentPathPlugin(),
49
+ tailwindcss(),
50
+ Vuetify({
51
+ autoImport: true,
52
+ }),
53
+ ],
41
54
  resolve: {
42
55
  extensions: [".js", ".ts", ".vue"],
43
56
  alias: {