Various build shenenigans (#5)
This PR extends the build pipeline of the www service to support locale-prefixed static builds (EN/RU) with build-time SSG output injection, while also refactoring UI typography to a tokenized Tailwind v4 @theme-driven approach. Changes: - Add build-time SSG plugin to @ermnvldmr/rsbuild-config and wire multi-locale builds (/en/, /ru/) into services/www. - Introduce new @ermnvldmr/i18n package (locale constants, path helpers, keysets, pluralization) and apply it to www navigation/text. - Refactor ui typography to a shared getTypographyClassNames utility + Tailwind token definitions, with stl/cn updated for correct Tailwind-merge behavior.
This commit is contained in:
1 parent
ac9c455e5a
commit
b84ef775b8
68 files changed
+2991
-397
No files matched your search
@@ -1,5 +1,20 @@
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { extendTailwindMerge } from 'tailwind-merge';
|
||||
|
||||
const RB_TYPES = ['display', 'headline', 'title', 'body', 'label'] as const;
|
||||
const RB_SIZES = ['l', 'm', 's'] as const;
|
||||
|
||||
const rbFontSizeClasses = RB_TYPES.flatMap((type) =>
|
||||
RB_SIZES.map((size) => `${type}-${size}`)
|
||||
);
|
||||
|
||||
const twMerge = extendTailwindMerge({
|
||||
extend: {
|
||||
classGroups: {
|
||||
'font-size': [{ 'text-rb': rbFontSizeClasses }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Combines and merges CSS class names using clsx and tailwind-merge.
|
||||
@@ -7,6 +22,10 @@ import { twMerge } from 'tailwind-merge';
|
||||
* This utility function combines the power of clsx for conditional class handling
|
||||
* with tailwind-merge for proper Tailwind CSS class deduplication and conflict resolution.
|
||||
*
|
||||
* Tailwind-merge is configured to recognise `text-rb-{type}-{size}` classes
|
||||
* (e.g. `text-rb-body-m`) as font-size utilities so they do not conflict with
|
||||
* text-color classes such as `text-[var(--rb-text)]`.
|
||||
*
|
||||
* @param inputs - Class values to combine (strings, objects, arrays, etc.)
|
||||
* @returns Merged class string with conflicts resolved
|
||||
*
|
||||
|
||||
@@ -39,9 +39,8 @@ export function useLocalStorage<T>(
|
||||
const getSnapshot = useCallback(() => manager.get(), [manager]);
|
||||
|
||||
const value = useSyncExternalStore(subscribe, getSnapshot, () => {
|
||||
// During SSR, we can't access localStorage.
|
||||
// However, createLocalStorage is designed to throw if accessed during SSR.
|
||||
// This hook is intended to be used in client-side components.
|
||||
// SSR snapshot: createLocalStorage returns a no-op manager in Node context,
|
||||
// so manager.get() safely returns defaultValue without accessing localStorage.
|
||||
return manager.get();
|
||||
});
|
||||
|
||||
|
||||
@@ -44,9 +44,15 @@ describe('lib/window/localStorage', () => {
|
||||
for (const key in mockStorage) delete mockStorage[key];
|
||||
});
|
||||
|
||||
it('should throw if window is undefined', () => {
|
||||
it('returns a no-op manager when window is undefined (SSR/SSG context)', () => {
|
||||
vi.stubGlobal('window', undefined);
|
||||
expect(() => createLocalStorage(KEY, DEFAULT_VALUE)).toThrow();
|
||||
const manager = createLocalStorage(KEY, DEFAULT_VALUE);
|
||||
expect(manager.key).toBe(KEY);
|
||||
expect(manager.get()).toEqual(DEFAULT_VALUE);
|
||||
expect(() => manager.set({ foo: 'x' })).not.toThrow();
|
||||
expect(() => manager.remove()).not.toThrow();
|
||||
// get() still returns defaultValue after set (no-op in SSR)
|
||||
expect(manager.get()).toEqual(DEFAULT_VALUE);
|
||||
});
|
||||
|
||||
it('should get default value if empty', () => {
|
||||
|
||||
@@ -17,7 +17,8 @@ export interface StorageManager<T> {
|
||||
/**
|
||||
* Creates a type-safe manager for a localStorage key.
|
||||
*
|
||||
* This utility is browser-only and will throw if accessed during SSR.
|
||||
* In SSR/SSG context (no window), returns a no-op manager that always
|
||||
* returns the default value. In the browser, uses localStorage as usual.
|
||||
* Ensure you use a strict namespace for the key to avoid monorepo collisions.
|
||||
* Recommended format: `ermnvldmr/{package}/{path}/{module}`
|
||||
* Example: `ermnvldmr/ui/lib/theme`
|
||||
@@ -31,15 +32,15 @@ export interface StorageManager<T> {
|
||||
* const theme = createLocalStorage('ermnvldmr/ui/lib/theme', 'system');
|
||||
* console.log(theme.get());
|
||||
* ```
|
||||
*
|
||||
* @throws Error if window or localStorage is undefined.
|
||||
*/
|
||||
export function createLocalStorage<T>(key: string, defaultValue: T): StorageManager<T> {
|
||||
if (typeof window === 'undefined' || typeof window.localStorage === 'undefined') {
|
||||
throw new Error(
|
||||
`[createLocalStorage] Attempted to access storage for key "${key}" outside of a browser environment. ` +
|
||||
'This utility is strictly for client-side use.'
|
||||
);
|
||||
return {
|
||||
key,
|
||||
get: () => defaultValue,
|
||||
set: () => { /* no-op in SSR/SSG context */ },
|
||||
remove: () => { /* no-op in SSR/SSG context */ },
|
||||
};
|
||||
}
|
||||
|
||||
const manager: StorageManager<T> = {
|
||||
|
||||
Reference in new issue
Block a user