@shellui/core 0.2.0-alpha.2 → 0.2.0-alpha.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shellui/core",
3
- "version": "0.2.0-alpha.2",
3
+ "version": "0.2.0-alpha.3",
4
4
  "description": "ShellUI Core - Core React application runtime",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -58,7 +58,7 @@
58
58
  "workbox-strategies": "^7.1.0",
59
59
  "workbox-cacheable-response": "^7.1.0",
60
60
  "workbox-expiration": "^7.1.0",
61
- "@shellui/sdk": "0.2.0-alpha.2"
61
+ "@shellui/sdk": "0.2.0-alpha.3"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "react": "^18.0.0 || ^19.0.0",
@@ -6,12 +6,13 @@ import {
6
6
  type Settings,
7
7
  type SettingsNavigationItem,
8
8
  type Appearance,
9
+ type SettingsAvailableTheme,
9
10
  } from '@shellui/sdk';
10
11
  import { SettingsContext } from './SettingsContext';
11
12
  import { useConfig } from '../config/useConfig';
12
13
  import { useTranslation } from 'react-i18next';
13
14
  import type { NavigationItem, NavigationGroup, ShellUIConfig } from '../config/types';
14
- import { getTheme, registerTheme } from '../theme/themes';
15
+ import { getTheme, getAllThemes, registerTheme } from '../theme/themes';
15
16
 
16
17
  const logger = getLogger('shellcore');
17
18
 
@@ -40,6 +41,20 @@ function resolveColorMode(colorScheme: 'light' | 'dark' | 'system'): 'light' | '
40
41
  return colorScheme === 'dark' ? 'dark' : 'light';
41
42
  }
42
43
 
44
+ /** Convert font file URLs to absolute so iframes/modals on other ports or domains can load them. */
45
+ function toAbsoluteFontUrls(urls: string[]): string[] {
46
+ if (typeof window === 'undefined') return urls;
47
+ const origin = window.location.origin;
48
+ return urls.map((url) => {
49
+ const trimmed = url.trim();
50
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
51
+ return trimmed;
52
+ }
53
+ const path = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
54
+ return `${origin}${path}`;
55
+ });
56
+ }
57
+
43
58
  /**
44
59
  * Build the full appearance object for settings propagation so apps receive all theme
45
60
  * variable values and can style without knowing the theme name.
@@ -75,13 +90,29 @@ function getResolvedAppearanceForSettings(
75
90
  ...(themeDef.textShadow !== undefined && { textShadow: themeDef.textShadow }),
76
91
  ...(themeDef.lineHeight !== undefined && { lineHeight: themeDef.lineHeight }),
77
92
  ...(themeDef.fontFiles !== undefined &&
78
- themeDef.fontFiles.length > 0 && { fontFiles: themeDef.fontFiles }),
93
+ themeDef.fontFiles.length > 0 && {
94
+ fontFiles: toAbsoluteFontUrls(themeDef.fontFiles),
95
+ }),
79
96
  };
80
97
  }
81
98
 
82
99
  /**
83
- * Build settings for propagation to iframes: inject navigation and full theme object
84
- * so apps receive all theme variable values.
100
+ * Map registered themes to the slim shape sent to sub-apps (name, displayName, colors, optional typography for preview).
101
+ */
102
+ function getAvailableThemesForSettings(): SettingsAvailableTheme[] {
103
+ return getAllThemes().map((theme) => ({
104
+ name: theme.name,
105
+ displayName: theme.displayName,
106
+ colors: theme.colors,
107
+ ...(theme.fontFamily !== undefined && { fontFamily: theme.fontFamily }),
108
+ ...(theme.letterSpacing !== undefined && { letterSpacing: theme.letterSpacing }),
109
+ ...(theme.textShadow !== undefined && { textShadow: theme.textShadow }),
110
+ }));
111
+ }
112
+
113
+ /**
114
+ * Build settings for propagation to iframes: inject navigation, full theme object,
115
+ * and list of available themes so apps can render theme pickers.
85
116
  */
86
117
  function buildSettingsForPropagation(
87
118
  settings: Settings,
@@ -93,6 +124,16 @@ function buildSettingsForPropagation(
93
124
  ...settings,
94
125
  appearance: appearance ?? settings.appearance,
95
126
  };
127
+ // Inject available themes when we have a resolved appearance (themes are already registered above)
128
+ if (result.appearance && typeof window !== 'undefined') {
129
+ result = {
130
+ ...result,
131
+ appearance: {
132
+ ...result.appearance,
133
+ availableThemes: getAvailableThemesForSettings(),
134
+ },
135
+ };
136
+ }
96
137
  if (config?.navigation?.length) {
97
138
  const items: SettingsNavigationItem[] = flattenNavigationItems(
98
139
  config.navigation,
@@ -4,8 +4,9 @@ import { useConfig } from '../../config/useConfig';
4
4
  import { Button } from '../../../components/ui/button';
5
5
  import { ButtonGroup } from '../../../components/ui/button-group';
6
6
  import { cn } from '../../../lib/utils';
7
- import { useEffect, useState } from 'react';
7
+ import { useEffect, useState, useMemo } from 'react';
8
8
  import { getAllThemes, registerTheme, type ThemeDefinition } from '../../theme/themes';
9
+ import type { SettingsAvailableTheme } from '@shellui/sdk';
9
10
 
10
11
  const SunIcon = () => (
11
12
  <svg
@@ -85,13 +86,19 @@ const MonitorIcon = () => (
85
86
  </svg>
86
87
  );
87
88
 
89
+ /** Theme-like shape used for preview (ThemeDefinition or SettingsAvailableTheme). */
90
+ type ThemePreviewItem = Pick<
91
+ ThemeDefinition | SettingsAvailableTheme,
92
+ 'name' | 'displayName' | 'colors' | 'fontFamily' | 'letterSpacing' | 'textShadow'
93
+ >;
94
+
88
95
  // Theme color preview component
89
96
  const ThemePreview = ({
90
97
  theme,
91
98
  isSelected,
92
99
  isDark,
93
100
  }: {
94
- theme: ThemeDefinition;
101
+ theme: ThemePreviewItem;
95
102
  isSelected: boolean;
96
103
  isDark: boolean;
97
104
  }) => {
@@ -169,18 +176,25 @@ export const Appearance = () => {
169
176
  const currentTheme = settings.appearance?.colorScheme ?? 'system';
170
177
  const currentThemeName = settings.appearance?.name ?? 'default';
171
178
 
172
- const [availableThemes, setAvailableThemes] = useState<ThemeDefinition[]>([]);
179
+ const [localThemes, setLocalThemes] = useState<ThemeDefinition[]>([]);
173
180
 
174
- // Register custom themes from config and get all themes
181
+ // Register custom themes from config and get all themes (for shell context)
175
182
  useEffect(() => {
176
183
  if (config?.themes) {
177
184
  config.themes.forEach((themeDef: ThemeDefinition) => {
178
185
  registerTheme(themeDef);
179
186
  });
180
187
  }
181
- setAvailableThemes(getAllThemes());
188
+ setLocalThemes(getAllThemes());
182
189
  }, [config]);
183
190
 
191
+ // Use availableThemes from settings when provided (e.g. from shell when in sub-app), else local registry
192
+ const availableThemes = useMemo((): ThemePreviewItem[] => {
193
+ const fromSettings = settings.appearance?.availableThemes;
194
+ if (fromSettings?.length) return fromSettings;
195
+ return localThemes;
196
+ }, [settings.appearance?.availableThemes, localThemes]);
197
+
184
198
  // Determine if we're in dark mode for preview
185
199
  const [isDarkForPreview, setIsDarkForPreview] = useState(() => {
186
200
  if (typeof window === 'undefined') return false;