@thangnqdev/ui 0.2.2 → 0.2.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/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Avatar, A
2
2
  export { Alert, AlertDescription, AlertTitle, CircularProgress, EmptyState, feedback_exports as Feedback, ProgressBar, Skeleton, SkeletonCircle, SkeletonText, Spinner, alertVariants, progressVariants, spinnerVariants } from './chunk-WAFHC4DQ.js';
3
3
  export { glass_exports as Glass, GlassBadge, GlassButton, GlassCard } from './chunk-A7P3O5Z5.js';
4
4
  export { bento_exports as Bento, BentoCard, BentoGrid } from './chunk-C4SUXMI3.js';
5
- export { core_exports as Core, ThemeProvider, useTheme } from './chunk-DUPPPGXY.js';
5
+ export { core_exports as Core, ThemeProvider, useTheme } from './chunk-2ANJFOIU.js';
6
6
  export { AspectRatio, Container, Divider, Flex, Grid, GridItem, HStack, layout_exports as Layout, PageHeader, PageLayout, PageSidebar, ScrollArea, Section, Spacer, Stack, VStack, containerVariants, dividerVariants, flexVariants, gridItemVariants, gridVariants, sectionVariants, stackVariants } from './chunk-2OXZK6IP.js';
7
7
  export { Blockquote, Code, CodeBlock, Heading, Link, Text, typography_exports as Typography, headingVariants, linkVariants, textVariants } from './chunk-ATYGDTMK.js';
8
8
  export { ButtonGroup, buttons_exports as Buttons, IconButton, SplitButton, ToggleButton, ToggleGroup, buttonGroupVariants, iconButtonVariants, toggleVariants } from './chunk-KUQWUTZA.js';
package/llms.txt CHANGED
@@ -1,7 +1,7 @@
1
1
  # @thangnqdev/ui — Full Architecture Component System Contract
2
2
 
3
3
  > Machine-readable specification for AI Coding Agents (Claude Code, Antigravity, Cursor, Qwen, Codex).
4
- > Package: `@thangnqdev/ui` (v0.2.2)
4
+ > Package: `@thangnqdev/ui` (v0.2.3)
5
5
  > Total Registered SSOT Components: 145
6
6
  > Aesthetic Core: Spatial Optics, Editorial Bento, and Enterprise-grade Accessible Primitives.
7
7
  > Sub-module Exports: `core`, `layout`, `typography`, `buttons`, `forms`, `overlays`, `navigation`, `data-display`, `feedback`, `glass`, `bento`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thangnqdev/ui",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "description": "Spatial Optics & Editorial Bento UI System with Machine-Readable AI Contracts",
6
6
  "license": "MIT",
@@ -1,4 +1,13 @@
1
- import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';
1
+ import React, {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useLayoutEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react';
2
11
 
3
12
  export type Theme = 'dark' | 'light' | 'system';
4
13
 
@@ -17,7 +26,13 @@ interface ThemeContextType {
17
26
  toggleTheme: () => void;
18
27
  }
19
28
 
20
- const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
29
+ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
30
+
31
+ const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
32
+
33
+ function isTheme(value: string | null): value is Theme {
34
+ return value === 'dark' || value === 'light' || value === 'system';
35
+ }
21
36
 
22
37
  export function ThemeProvider({
23
38
  children,
@@ -28,9 +43,10 @@ export function ThemeProvider({
28
43
  }: ThemeProviderProps) {
29
44
  // Always initialize identically on SSR and initial client pass to avoid hydration mismatches
30
45
  const [theme, setThemeState] = useState<Theme>(defaultTheme);
31
- const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>(
46
+ const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>(
32
47
  defaultTheme === 'dark' ? 'dark' : 'light'
33
- );
48
+ );
49
+ const pendingStoredThemeRef = useRef<{ storageKey: string; theme: Theme } | null>(null);
34
50
 
35
51
  const computeResolved = useCallback(
36
52
  (t: Theme): 'dark' | 'light' => {
@@ -42,69 +58,71 @@ export function ThemeProvider({
42
58
  [enableSystem]
43
59
  );
44
60
 
45
- // Client-side initialization: read storage once mounted
46
- useEffect(() => {
47
- if (typeof window === 'undefined') return;
48
-
49
- try {
61
+ const applyResolvedTheme = useCallback(
62
+ (nextResolvedTheme: 'dark' | 'light') => {
63
+ setResolvedTheme(nextResolvedTheme);
64
+
65
+ if (typeof document === 'undefined') return;
66
+
67
+ const root = document.documentElement;
68
+ root.setAttribute(attribute, nextResolvedTheme);
69
+ root.classList.toggle('dark', nextResolvedTheme === 'dark');
70
+ root.classList.toggle('light', nextResolvedTheme === 'light');
71
+ },
72
+ [attribute]
73
+ );
74
+
75
+ // Read the persisted value before paint. The pending ref prevents the DOM/storage
76
+ // sync below from writing defaultTheme over a different persisted theme first.
77
+ useIsomorphicLayoutEffect(() => {
78
+ if (typeof window === 'undefined') return;
79
+
80
+ let initialTheme = defaultTheme;
81
+ try {
50
82
  const stored = window.localStorage?.getItem(storageKey);
51
- if (stored === 'dark' || stored === 'light' || stored === 'system') {
52
- setThemeState(stored);
53
- }
54
- } catch {
55
- // Ignore storage access errors in restricted iframe/incognito environments
56
- }
83
+ if (isTheme(stored)) initialTheme = stored;
84
+ } catch {
85
+ // Ignore storage access errors in restricted iframe/incognito environments
86
+ }
87
+
88
+ pendingStoredThemeRef.current = { storageKey, theme: initialTheme };
89
+ setThemeState((currentTheme) => (currentTheme === initialTheme ? currentTheme : initialTheme));
57
90
  }, [storageKey]);
58
-
59
- // Sync theme changes to DOM and storage
60
- useEffect(() => {
61
- if (typeof document === 'undefined') return;
62
-
63
- const root = document.documentElement;
64
- const currentResolved = computeResolved(theme);
65
- setResolvedTheme(currentResolved);
66
-
67
- root.setAttribute(attribute, currentResolved);
68
- if (currentResolved === 'dark') {
69
- root.classList.add('dark');
70
- root.classList.remove('light');
71
- } else {
72
- root.classList.add('light');
73
- root.classList.remove('dark');
74
- }
75
-
76
- if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
77
- try {
91
+
92
+ // Apply user changes before paint. Storage initialization and user updates now
93
+ // have a single write direction, so a click cannot oscillate light <-> dark.
94
+ useIsomorphicLayoutEffect(() => {
95
+ const pendingStoredTheme = pendingStoredThemeRef.current;
96
+ if (pendingStoredTheme?.storageKey === storageKey) {
97
+ if (pendingStoredTheme.theme !== theme) return;
98
+ pendingStoredThemeRef.current = null;
99
+ }
100
+
101
+ const currentResolved = computeResolved(theme);
102
+ applyResolvedTheme(currentResolved);
103
+
104
+ if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {
105
+ try {
78
106
  window.localStorage.setItem(storageKey, theme);
79
107
  } catch {
80
- // Ignore storage access errors
81
- }
82
- }
83
- }, [theme, attribute, computeResolved, storageKey]);
108
+ // Ignore storage access errors
109
+ }
110
+ }
111
+ }, [theme, computeResolved, applyResolvedTheme, storageKey]);
84
112
 
85
113
  // System media query listener
86
114
  useEffect(() => {
87
115
  if (theme !== 'system' || !enableSystem || typeof window === 'undefined' || !window.matchMedia) return;
88
116
 
89
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
90
- const handleChange = () => {
91
- const newResolved = mediaQuery.matches ? 'dark' : 'light';
92
- setResolvedTheme(newResolved);
93
- if (typeof document !== 'undefined') {
94
- document.documentElement.setAttribute(attribute, newResolved);
95
- if (newResolved === 'dark') {
96
- document.documentElement.classList.add('dark');
97
- document.documentElement.classList.remove('light');
98
- } else {
99
- document.documentElement.classList.add('light');
100
- document.documentElement.classList.remove('dark');
101
- }
102
- }
103
- };
117
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
118
+ const handleChange = () => {
119
+ const newResolved = mediaQuery.matches ? 'dark' : 'light';
120
+ applyResolvedTheme(newResolved);
121
+ };
104
122
 
105
- mediaQuery.addEventListener('change', handleChange);
106
- return () => mediaQuery.removeEventListener('change', handleChange);
107
- }, [theme, enableSystem, attribute]);
123
+ mediaQuery.addEventListener('change', handleChange);
124
+ return () => mediaQuery.removeEventListener('change', handleChange);
125
+ }, [theme, enableSystem, applyResolvedTheme]);
108
126
 
109
127
  const toggleTheme = () => {
110
128
  setThemeState((prev) => {
package/ui-contract.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "packageName": "@thangnqdev/ui",
4
- "version": "0.2.2",
4
+ "version": "0.2.3",
5
5
  "totalComponents": 145,
6
6
  "modules": {
7
7
  "core": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/index.ts","../src/core/ThemeProvider.tsx"],"names":[],"mappings":"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","file":"chunk-DUPPPGXY.js","sourcesContent":["export * from './ThemeProvider';\r\nexport * from '../utils/cn';\r\n","import React, { createContext, useContext, useEffect, useState, useMemo, useCallback } from 'react';\r\n\r\nexport type Theme = 'dark' | 'light' | 'system';\r\n\r\nexport interface ThemeProviderProps {\r\n children: React.ReactNode;\r\n defaultTheme?: Theme;\r\n storageKey?: string;\r\n attribute?: string;\r\n enableSystem?: boolean;\r\n}\r\n\r\ninterface ThemeContextType {\r\n theme: Theme;\r\n resolvedTheme: 'dark' | 'light';\r\n setTheme: (theme: Theme) => void;\r\n toggleTheme: () => void;\r\n}\r\n\r\nconst ThemeContext = createContext<ThemeContextType | undefined>(undefined);\r\n\r\nexport function ThemeProvider({\r\n children,\r\n defaultTheme = 'light',\n storageKey = 'ui-theme',\r\n attribute = 'data-theme',\r\n enableSystem = true,\r\n}: ThemeProviderProps) {\r\n // Always initialize identically on SSR and initial client pass to avoid hydration mismatches\r\n const [theme, setThemeState] = useState<Theme>(defaultTheme);\r\n const [resolvedTheme, setResolvedTheme] = useState<'dark' | 'light'>(\r\n defaultTheme === 'dark' ? 'dark' : 'light'\n );\r\n\r\n const computeResolved = useCallback(\r\n (t: Theme): 'dark' | 'light' => {\r\n if (t === 'system' && enableSystem && typeof window !== 'undefined' && window.matchMedia) {\r\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\r\n }\r\n return t === 'dark' ? 'dark' : 'light';\r\n },\r\n [enableSystem]\r\n );\r\n\r\n // Client-side initialization: read storage once mounted\r\n useEffect(() => {\r\n if (typeof window === 'undefined') return;\r\n\r\n try {\r\n const stored = window.localStorage?.getItem(storageKey);\n if (stored === 'dark' || stored === 'light' || stored === 'system') {\n setThemeState(stored);\n }\n } catch {\r\n // Ignore storage access errors in restricted iframe/incognito environments\r\n }\r\n }, [storageKey]);\n\r\n // Sync theme changes to DOM and storage\r\n useEffect(() => {\r\n if (typeof document === 'undefined') return;\r\n\r\n const root = document.documentElement;\r\n const currentResolved = computeResolved(theme);\r\n setResolvedTheme(currentResolved);\r\n\r\n root.setAttribute(attribute, currentResolved);\r\n if (currentResolved === 'dark') {\r\n root.classList.add('dark');\r\n root.classList.remove('light');\r\n } else {\r\n root.classList.add('light');\r\n root.classList.remove('dark');\r\n }\r\n\r\n if (typeof window !== 'undefined' && typeof window.localStorage !== 'undefined') {\r\n try {\r\n window.localStorage.setItem(storageKey, theme);\r\n } catch {\r\n // Ignore storage access errors\r\n }\r\n }\r\n }, [theme, attribute, computeResolved, storageKey]);\r\n\r\n // System media query listener\r\n useEffect(() => {\r\n if (theme !== 'system' || !enableSystem || typeof window === 'undefined' || !window.matchMedia) return;\r\n\r\n const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\r\n const handleChange = () => {\r\n const newResolved = mediaQuery.matches ? 'dark' : 'light';\r\n setResolvedTheme(newResolved);\r\n if (typeof document !== 'undefined') {\r\n document.documentElement.setAttribute(attribute, newResolved);\r\n if (newResolved === 'dark') {\r\n document.documentElement.classList.add('dark');\r\n document.documentElement.classList.remove('light');\r\n } else {\r\n document.documentElement.classList.add('light');\r\n document.documentElement.classList.remove('dark');\r\n }\r\n }\r\n };\r\n\r\n mediaQuery.addEventListener('change', handleChange);\r\n return () => mediaQuery.removeEventListener('change', handleChange);\r\n }, [theme, enableSystem, attribute]);\r\n\r\n const toggleTheme = () => {\r\n setThemeState((prev) => {\r\n if (prev === 'dark') return 'light';\r\n return 'dark';\r\n });\r\n };\r\n\r\n const value = useMemo(\r\n () => ({\r\n theme,\r\n resolvedTheme,\r\n setTheme: setThemeState,\r\n toggleTheme,\r\n }),\r\n [theme, resolvedTheme]\r\n );\r\n\r\n return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;\r\n}\r\n\r\nexport function useTheme(): ThemeContextType {\r\n const context = useContext(ThemeContext);\r\n if (!context) {\r\n throw new Error('useTheme must be used within a ThemeProvider');\r\n }\r\n return context;\r\n}\r\n"]}