@udixio/ui-react 2.10.11 → 2.10.13

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.
@@ -43,13 +43,9 @@ export const ProgressIndicator = ({
43
43
  };
44
44
 
45
45
  useEffect(() => {
46
- if (
47
- (variant === 'circular-indeterminate' ||
48
- variant === 'linear-indeterminate') &&
49
- completedPercentage !== 100
50
- ) {
46
+ if (variant === 'circular-indeterminate' && completedPercentage !== 100) {
51
47
  const interval = setInterval(() => {
52
- setCompletedPercentage(togglePercentage ? 10 : 90);
48
+ setCompletedPercentage(togglePercentage ? 20 : 40);
53
49
  setTogglePercentage(!togglePercentage);
54
50
  }, getTransitionRotate() * 1000);
55
51
  return () => clearInterval(interval);
@@ -83,26 +79,62 @@ export const ProgressIndicator = ({
83
79
 
84
80
  return (
85
81
  <>
86
- {(variant === 'linear-determinate' ||
87
- variant == 'linear-indeterminate') && (
82
+ {variant === 'linear-indeterminate' && (
83
+ <div className={styles.progressIndicator} {...restProps}>
84
+ <motion.div
85
+ animate={{
86
+ width: ['0%', '0%', '0%', '20%'],
87
+ marginLeft: ['0px', '0px', '6px', '6px'],
88
+ marginRight: ['0px', '0px', '6px', '6px'],
89
+ }}
90
+ transition={{
91
+ duration: 1.5,
92
+ repeat: Infinity,
93
+ ease: 'easeInOut',
94
+ times: [0, 0.499, 0.5, 1],
95
+ }}
96
+ style={{ flexShrink: 0 }}
97
+ className={styles.activeIndicator}
98
+ />
99
+ <motion.div
100
+ animate={{ width: ['0%', '40%', '100%'] }}
101
+ transition={{ duration: 1.5, repeat: Infinity, ease: 'easeInOut' }}
102
+ style={{ flexShrink: 0 }}
103
+ className={styles.firstTrack}
104
+ />
105
+ <motion.div
106
+ animate={{ width: ['20%', '60%', '20%'] }}
107
+ transition={{
108
+ duration: 1.5,
109
+ repeat: Infinity,
110
+ ease: 'easeInOut',
111
+ times: [0, 0.5, 1],
112
+ }}
113
+ style={{ flexShrink: 0, marginLeft: '6px' }}
114
+ className={styles.activeIndicator}
115
+ />
116
+ <div style={{ marginLeft: '6px' }} className={styles.lastTrack} />
117
+ </div>
118
+ )}
119
+ {variant === 'linear-determinate' && (
88
120
  <div className={styles.progressIndicator} {...restProps}>
89
121
  <div
90
122
  style={{
91
123
  width: `${completedPercentage}%`,
92
124
  transition: `width ${transitionDuration}ms ease-in-out ${completedPercentage == 100 ? ', max-height 200ms 0.5s ease-in-out' : ''}`,
93
125
  }}
94
- className={styles.track}
126
+ className={styles.activeIndicator}
95
127
  ></div>
96
128
  <div
97
129
  style={{
98
130
  marginLeft: completedPercentage != 100 ? '6px' : '0px',
99
131
  transition: `width ${transitionDuration}ms ease-in-out ${completedPercentage == 100 ? `, max-height 200ms 0.5s ease-in-out, margin-left ${transitionDuration}ms ${transitionDuration / 1.5}ms` : ''}`,
100
132
  }}
101
- className={styles.activeIndicator}
133
+ className={styles.lastTrack}
102
134
  ></div>
103
135
  <div
104
136
  style={{
105
- width: `4 px`,
137
+ width: `4px`,
106
138
  transition: `width ${transitionDuration}ms ease-in-out, max-height 200ms 0.5s ease-in-out`,
107
139
  }}
108
140
  className={styles.stop}
@@ -112,17 +144,25 @@ export const ProgressIndicator = ({
112
144
  {(variant === 'circular-determinate' ||
113
145
  variant == 'circular-indeterminate') && (
114
146
  <motion.svg
115
- key={togglePercentage + ''}
147
+ key={
148
+ variant === 'circular-indeterminate'
149
+ ? togglePercentage + ''
150
+ : 'static'
151
+ }
116
152
  width="48"
117
153
  height="48"
118
154
  viewBox="0 0 48 48"
119
155
  initial={{ rotate: -90 }}
120
- animate={{ rotate: 270 }}
121
- transition={{
122
- repeat: Infinity,
123
- duration: getTransitionRotate(),
124
- ease: 'linear',
125
- }}
156
+ animate={{ rotate: variant === 'circular-indeterminate' ? 270 : -90 }}
157
+ transition={
158
+ variant === 'circular-indeterminate'
159
+ ? {
160
+ repeat: Infinity,
161
+ duration: getTransitionRotate(),
162
+ ease: 'linear',
163
+ }
164
+ : { duration: transitionDuration / 1000 }
165
+ }
126
166
  className={styles.progressIndicator}
127
167
  {...(restProps as any)}
128
168
  >
@@ -133,8 +173,12 @@ export const ProgressIndicator = ({
133
173
  style={{
134
174
  strokeLinecap: 'round',
135
175
  }}
136
- initial="hidden"
137
- animate="visible"
176
+ initial={
177
+ variant === 'circular-indeterminate' ? 'hidden' : 'determinate'
178
+ }
179
+ animate={
180
+ variant === 'circular-indeterminate' ? 'visible' : 'determinate'
181
+ }
138
182
  className={styles.activeIndicator}
139
183
  variants={{
140
184
  hidden: {
@@ -143,14 +187,24 @@ export const ProgressIndicator = ({
143
187
  visible: {
144
188
  pathLength: togglePercentage ? 90 / 100 : 10 / 100,
145
189
  },
190
+ determinate: {
191
+ pathLength: completedPercentage / 100,
192
+ },
146
193
  }}
147
194
  transition={{
148
- pathLength: {
149
- type: 'tween',
150
- ease: 'linear',
151
- duration: getTransitionRotate(),
152
- bounce: 0,
153
- },
195
+ pathLength:
196
+ variant === 'circular-indeterminate'
197
+ ? {
198
+ type: 'tween',
199
+ ease: 'linear',
200
+ duration: getTransitionRotate(),
201
+ bounce: 0,
202
+ }
203
+ : {
204
+ type: 'tween',
205
+ ease: 'easeInOut',
206
+ duration: transitionDuration / 1000,
207
+ },
154
208
  }}
155
209
  />
156
210
  </motion.svg>
@@ -2,10 +2,16 @@ import {
2
2
  type API,
3
3
  type ConfigInterface,
4
4
  ContextOptions,
5
+ FontPlugin,
5
6
  loader,
7
+ serializeThemeContext,
6
8
  } from '@udixio/theme';
7
9
  import { useEffect, useRef, useState } from 'react';
8
10
  import { TailwindPlugin } from '@udixio/tailwind';
11
+ import type {
12
+ WorkerInboundMessage,
13
+ WorkerOutboundMessage,
14
+ } from './theme.worker';
9
15
 
10
16
  function isValidHexColor(hexColorString: string) {
11
17
  const regex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
@@ -14,7 +20,7 @@ function isValidHexColor(hexColorString: string) {
14
20
 
15
21
  export const ThemeProvider = ({
16
22
  config,
17
- throttleDelay = 100, // Délai par défaut de 300ms
23
+ throttleDelay = 100,
18
24
  onLoad,
19
25
  loadTheme = false,
20
26
  }: {
@@ -24,16 +30,51 @@ export const ThemeProvider = ({
24
30
  loadTheme?: boolean;
25
31
  }) => {
26
32
  const [themeApi, setThemeApi] = useState<API | null>(null);
33
+ const [outputCss, setOutputCss] = useState<string | null>(null);
34
+
35
+ const workerRef = useRef<Worker | null>(null);
36
+ const generationRef = useRef(0);
37
+ const lastAppliedIdRef = useRef(0);
38
+ const themeApiRef = useRef<API | null>(null);
39
+ const firstLoadDoneRef = useRef(false);
40
+ const onLoadRef = useRef(onLoad);
41
+ useEffect(() => {
42
+ onLoadRef.current = onLoad;
43
+ }, [onLoad]);
27
44
 
28
- // Charger l'API du thème une fois au montage
45
+ // Initialisation de l'API et du Worker
29
46
  useEffect(() => {
47
+ let cancelled = false;
48
+
30
49
  (async () => {
31
50
  const api = await loader(config, loadTheme);
51
+ if (cancelled) return;
52
+
53
+ themeApiRef.current = api;
32
54
  setThemeApi(api);
55
+
56
+ const worker = new Worker(
57
+ new URL('./theme.worker.ts', import.meta.url),
58
+ { type: 'module' },
59
+ );
60
+ workerRef.current = worker;
61
+
62
+ worker.onmessage = (e: MessageEvent<WorkerOutboundMessage>) => {
63
+ if (e.data.id > lastAppliedIdRef.current) {
64
+ lastAppliedIdRef.current = e.data.id;
65
+ firstLoadDoneRef.current = true;
66
+ setOutputCss(e.data.css);
67
+ onLoadRef.current?.(themeApiRef.current!);
68
+ }
69
+ };
33
70
  })();
34
- }, []);
35
71
 
36
- const [outputCss, setOutputCss] = useState<string | null>(null);
72
+ return () => {
73
+ cancelled = true;
74
+ workerRef.current?.terminate();
75
+ workerRef.current = null;
76
+ };
77
+ }, []);
37
78
 
38
79
  // Throttle avec exécution en tête (leading) et en fin (trailing)
39
80
  const timeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -41,11 +82,10 @@ export const ThemeProvider = ({
41
82
  const lastArgsRef = useRef<Partial<ContextOptions> | null>(null);
42
83
 
43
84
  useEffect(() => {
44
- if (!themeApi) return; // Attendre que l'API soit prête
85
+ if (!themeApi) return;
45
86
 
46
87
  const ctx: Partial<ContextOptions> = {
47
88
  ...config,
48
- // Assurer la compatibilité avec l'API qui attend sourceColorHex
49
89
  sourceColor: config.sourceColor,
50
90
  };
51
91
 
@@ -53,11 +93,9 @@ export const ThemeProvider = ({
53
93
  const timeSinceLast = now - lastExecTimeRef.current;
54
94
 
55
95
  const invoke = async (args: Partial<ContextOptions>) => {
56
- // applique et notifie
57
96
  await applyThemeChange(args);
58
97
  };
59
98
 
60
- // Leading: si délai écoulé ou jamais exécuté, exécuter tout de suite
61
99
  if (lastExecTimeRef.current === 0 || timeSinceLast >= throttleDelay) {
62
100
  if (timeoutRef.current) {
63
101
  clearTimeout(timeoutRef.current);
@@ -67,7 +105,6 @@ export const ThemeProvider = ({
67
105
  lastExecTimeRef.current = now;
68
106
  void invoke(ctx);
69
107
  } else {
70
- // Sinon, mémoriser la dernière requête et programmer une exécution en trailing
71
108
  lastArgsRef.current = ctx;
72
109
  if (!timeoutRef.current) {
73
110
  const remaining = Math.max(0, throttleDelay - timeSinceLast);
@@ -83,34 +120,43 @@ export const ThemeProvider = ({
83
120
  }
84
121
  }
85
122
 
86
- // Cleanup: au changement de dépendances, ne rien faire ici (on gère trailing)
87
123
  return () => {};
88
124
  }, [config, throttleDelay, themeApi]);
89
125
 
90
126
  const applyThemeChange = async (ctx: Partial<ContextOptions>) => {
91
- if (typeof ctx.sourceColor == 'string') {
92
- if (!isValidHexColor(ctx.sourceColor)) {
93
- throw new Error('Invalid hex color');
94
- }
127
+ if (typeof ctx.sourceColor === 'string' && !isValidHexColor(ctx.sourceColor)) {
128
+ throw new Error('Invalid hex color');
95
129
  }
96
130
 
97
- if (!themeApi) {
98
- // L'API n'est pas prête; ignorer silencieusement car l'effet principal attend themeApi
99
- return;
100
- }
101
- themeApi.context.update(ctx);
131
+ const api = themeApiRef.current;
132
+ if (!api) return;
133
+
134
+ // Toujours évaluer sur le main thread (rapide)
135
+ api.context.update(ctx);
136
+ api.palettes.sync((ctx as any).palettes);
102
137
 
103
- await themeApi.load();
138
+ const worker = workerRef.current;
104
139
 
105
- const outputCss = themeApi?.plugins
106
- .getPlugin(TailwindPlugin)
107
- .getInstance().outputCss;
108
- setOutputCss(outputCss);
140
+ // Fallback synchrone : premier rendu ou Worker pas encore prêt
141
+ if (!worker || !firstLoadDoneRef.current) {
142
+ await api.load();
143
+ const css = api.plugins.getPlugin(TailwindPlugin).getInstance().outputCss;
144
+ setOutputCss(css);
145
+ firstLoadDoneRef.current = true;
146
+ onLoad?.(api);
147
+ return;
148
+ }
109
149
 
110
- onLoad?.(themeApi);
150
+ // Offload au Worker
151
+ const id = ++generationRef.current;
152
+ worker.postMessage({
153
+ id,
154
+ snapshot: serializeThemeContext(api),
155
+ tailwindOptions: api.plugins.getPlugin(TailwindPlugin).options,
156
+ fontOptions: api.plugins.getPlugin(FontPlugin).options,
157
+ } satisfies WorkerInboundMessage);
111
158
  };
112
159
 
113
- // Cleanup lors du démontage du composant
114
160
  useEffect(() => {
115
161
  return () => {
116
162
  if (timeoutRef.current) {
@@ -0,0 +1,97 @@
1
+ import {
2
+ loader,
3
+ getVariantByName,
4
+ Hct,
5
+ FontPlugin,
6
+ } from '@udixio/theme';
7
+ import type { API, FontPluginOptions, ThemeContextSnapshot } from '@udixio/theme';
8
+ import { TailwindPlugin } from '@udixio/tailwind';
9
+ import type { TailwindPluginOptions } from '@udixio/tailwind';
10
+
11
+ export interface WorkerInboundMessage {
12
+ id: number;
13
+ snapshot: ThemeContextSnapshot;
14
+ tailwindOptions: TailwindPluginOptions;
15
+ fontOptions: FontPluginOptions;
16
+ }
17
+
18
+ export interface WorkerOutboundMessage {
19
+ id: number;
20
+ css: string;
21
+ }
22
+
23
+ let workerApi: API | null = null;
24
+ let latestMessage: WorkerInboundMessage | null = null;
25
+ let processing = false;
26
+
27
+ async function processLatest() {
28
+ if (processing || !latestMessage) return;
29
+ processing = true;
30
+
31
+ const msg = latestMessage;
32
+ latestMessage = null;
33
+
34
+ const { snapshot, tailwindOptions, fontOptions } = msg;
35
+
36
+ const palettesCallbacks = Object.fromEntries(
37
+ Object.entries(snapshot.palettes).map(([key, { hue, chroma }]) => [
38
+ key,
39
+ () => ({ hue, chroma }),
40
+ ]),
41
+ );
42
+
43
+ try {
44
+ if (!workerApi) {
45
+ // Initialisation unique — coût amorti sur tous les messages suivants
46
+ workerApi = await loader(
47
+ {
48
+ sourceColor: Hct.from(
49
+ snapshot.sourceColor.hue,
50
+ snapshot.sourceColor.chroma,
51
+ snapshot.sourceColor.tone,
52
+ ),
53
+ isDark: snapshot.isDark,
54
+ contrastLevel: snapshot.contrastLevel,
55
+ variant: getVariantByName(snapshot.variantName),
56
+ plugins: [new FontPlugin(fontOptions), new TailwindPlugin(tailwindOptions)],
57
+ },
58
+ false,
59
+ );
60
+ workerApi.palettes.sync(palettesCallbacks);
61
+ } else {
62
+ // Mise à jour légère — pas de re-bootstrap
63
+ workerApi.context.update({
64
+ isDark: snapshot.isDark,
65
+ contrastLevel: snapshot.contrastLevel,
66
+ sourceColor: Hct.from(
67
+ snapshot.sourceColor.hue,
68
+ snapshot.sourceColor.chroma,
69
+ snapshot.sourceColor.tone,
70
+ ),
71
+ variant: getVariantByName(snapshot.variantName),
72
+ });
73
+ workerApi.palettes.sync(palettesCallbacks);
74
+
75
+ // Mise à jour des options plugins si elles ont changé
76
+ workerApi.plugins.getPlugin(TailwindPlugin).options = tailwindOptions;
77
+ workerApi.plugins.getPlugin(FontPlugin).options = fontOptions;
78
+ }
79
+
80
+ await workerApi.load();
81
+
82
+ const css = workerApi.plugins.getPlugin(TailwindPlugin).getInstance().outputCss;
83
+ self.postMessage({ id: msg.id, css } satisfies WorkerOutboundMessage);
84
+ } catch (e) {
85
+ console.error('[Worker] error during processLatest:', e);
86
+ workerApi = null; // reset state for clean retry
87
+ } finally {
88
+ processing = false;
89
+ // Traite le prochain message s'il est arrivé pendant le traitement
90
+ processLatest();
91
+ }
92
+ }
93
+
94
+ self.onmessage = (event: MessageEvent<WorkerInboundMessage>) => {
95
+ latestMessage = event.data;
96
+ processLatest();
97
+ };
@@ -31,5 +31,11 @@ export interface ProgressIndicatorInterface {
31
31
  };
32
32
  states: { isVisible: boolean };
33
33
 
34
- elements: ['progressIndicator', 'stop', 'activeIndicator', 'track'];
34
+ elements: [
35
+ 'progressIndicator',
36
+ 'stop',
37
+ 'activeIndicator',
38
+ 'lastTrack',
39
+ 'firstTrack',
40
+ ];
35
41
  }
@@ -8,7 +8,7 @@ import {
8
8
 
9
9
  const cardConfig: ClassNameComponent<CardInterface> = ({
10
10
  variant,
11
- isInteractive,
11
+ interactive,
12
12
  }) => ({
13
13
  card: classNames(
14
14
  ' rounded-xl overflow-hidden ',
@@ -16,7 +16,7 @@ const cardConfig: ClassNameComponent<CardInterface> = ({
16
16
  variant === 'elevated' && 'bg-surface-container-low shadow-1',
17
17
  variant === 'filled' && 'bg-surface-container-highest',
18
18
  {
19
- 'group/card': isInteractive,
19
+ 'group/card cursor-pointer': interactive,
20
20
  },
21
21
  ),
22
22
  });
@@ -6,13 +6,9 @@ import {
6
6
  defaultClassNames,
7
7
  } from '../utils';
8
8
 
9
- export const carouselItemConfig: ClassNameComponent<CarouselItemInterface> = ({
10
- width,
11
- }) => {
9
+ export const carouselItemConfig: ClassNameComponent<CarouselItemInterface> = () => {
12
10
  return {
13
11
  carouselItem: classNames('rounded-[28px] overflow-hidden flex-none', {
14
- hidden: width === undefined,
15
- 'flex-1': width == null,
16
12
  }),
17
13
  };
18
14
  };
@@ -12,19 +12,27 @@ const progressIndicatorConfig: ClassNameComponent<
12
12
  progressIndicator: classNames(
13
13
  (variant === 'linear-determinate' || variant == 'linear-indeterminate') &&
14
14
  'flex w-full h-1',
15
+ variant === 'linear-indeterminate' &&
16
+ 'relative overflow-hidden rounded-full',
15
17
  ),
16
- track: classNames('h-full rounded-full bg-primary rounded-l-full', {
17
- 'max-h-0': !isVisible,
18
- 'max-h-full': isVisible,
19
- }),
20
- activeIndicator: classNames(
18
+ firstTrack: classNames(
19
+ (variant === 'linear-determinate' || variant === 'linear-indeterminate') &&
20
+ 'h-full rounded-full bg-primary-container',
21
21
  {
22
22
  'max-h-0': !isVisible,
23
23
  'max-h-full': isVisible,
24
24
  },
25
- (variant === 'linear-determinate' || variant == 'linear-indeterminate') &&
26
- 'h-full flex-1 rounded-full bg-primary-container',
27
-
25
+ ),
26
+ activeIndicator: classNames(
27
+ 'h-full rounded-full bg-primary',
28
+ variant === 'linear-determinate' && {
29
+ 'rounded-l-full': true,
30
+ },
31
+ (variant === 'linear-indeterminate' ||
32
+ variant === 'linear-determinate') && {
33
+ 'max-h-0': !isVisible,
34
+ 'max-h-full': isVisible,
35
+ },
28
36
  (variant === 'circular-determinate' ||
29
37
  variant == 'circular-indeterminate') && [
30
38
  'stroke-primary fill-transparent ',
@@ -34,6 +42,14 @@ const progressIndicatorConfig: ClassNameComponent<
34
42
  },
35
43
  ],
36
44
  ),
45
+ lastTrack: classNames(
46
+ (variant === 'linear-determinate' || variant == 'linear-indeterminate') &&
47
+ 'h-full flex-1 rounded-full bg-primary-container',
48
+ {
49
+ 'max-h-0': !isVisible,
50
+ 'max-h-full': isVisible,
51
+ },
52
+ ),
37
53
  stop: classNames(
38
54
  'absolute top-1/2 -translate-y-1/2 right-0 bg-primary rounded-full size-1',
39
55
  {
package/vite.config.ts CHANGED
@@ -17,6 +17,7 @@ const getUdixioVite = async () => {
17
17
 
18
18
  export default defineConfig(async () => ({
19
19
  root: __dirname,
20
+ base: './',
20
21
  cacheDir: '../../node_modules/.vite/packages/ui-react',
21
22
  plugins: [
22
23
  await getUdixioVite(),
@@ -32,10 +33,14 @@ export default defineConfig(async () => ({
32
33
  brotliSize: true,
33
34
  }),
34
35
  ],
35
- // Uncomment this if you are using workers.
36
- // worker: {
37
- // plugins: [ nxViteTsPaths() ],
38
- // },
36
+ worker: {
37
+ format: 'es' as const,
38
+ rollupOptions: {
39
+ output: {
40
+ entryFileNames: '[name].js',
41
+ },
42
+ },
43
+ },
39
44
  // Configuration for building your library.
40
45
  // See: https://vitejs.dev/guide/build.html#library-mode
41
46
  build: {
@@ -56,6 +61,11 @@ export default defineConfig(async () => ({
56
61
  formats: ['es' as const, 'cjs' as const],
57
62
  },
58
63
  rollupOptions: {
64
+ output: {
65
+ // Worker JS emis sans hash pour que la référence dans dist/index.js soit stable
66
+ assetFileNames: '[name][extname]',
67
+ chunkFileNames: '[name].js',
68
+ },
59
69
  // External packages that should not be bundled into your library.
60
70
  external: [
61
71
  'react',