npm package: @hxnova/themes
Description: This package provides a Material Design 3 based theming system for styling Nova applications.
Latest version: 1.2.0

# @hxnova/themes

This package provides a Material Design 3 based theming system for styling Nova applications.


## Installation

```bash
# With npm
npm install @hxnova/themes@beta

# With pnpm
pnpm add @hxnova/themes@beta

# With yarn
yarn add @hxnova/themes@beta
```

## Usage

### Configuring the theme

Import the required stylesheets and set up the theme provider in your application entry point:

```typescript
import React from 'react';
import '@hxnova/themes/styles.css';
import '@hxnova/icons/icons.css';
import { CssBaseline } from '@hxnova/react-components/CssBaseline';
import { ThemeProvider } from '@hxnova/react-components/styles';
import novaTheme from '@hxnova/themes';

<ThemeProvider theme={novaTheme}>
  <CssBaseline />
   {/* Your Application Contents Here */}
</ThemeProvider>
```

### Fonts And additional CSS style

The `styles.css` file includes:
- Hexagon Akkurat font styles for Typography components
- Viewport related tokens (radius, spacing, height, icon size, outline, and component dimensions)

> Note: Make sure your project is licensed to use the Hexagon Akkurat font.

```
// index.css
@import '@hxnova/themes/styles.css';

// index.js
import '@hxnova/themes/styles.css';
```

### Support Light & Dark Theme

Use the `useColorScheme` hook to toggle between light and dark themes:

```typescript
import React from 'react';
import { useColorScheme } from '@hxnova/react-components/styles';
import { IconButton } from '@hxnova/react-components/IconButton';

function ColorSchemeToggleButton() {
  const { mode, setMode } = useColorScheme();

  const toggleColorScheme = () => {
    setMode(mode === 'dark' ? 'light' : 'dark');
  };

  return (
    <IconButton onClick={toggleColorScheme}>
      {mode === 'light' ? '🌙' : '🔆'}
    </IconButton>
  );
}
```

### Use the theme

#### Using styled components

```typescript
import { styled } from '@hxnova/react-components/styled';

const StyledComponent = styled('div')(({ theme }) => ({
  backgroundColor: theme.vars.palette.primary,
  color: theme.vars.palette.onPrimary,
  borderRadius: theme.vars.sys.size.radius.md.medium,
  boxShadow: theme.shadows[2],
  padding: 2,
  [theme.breakpoints.up('md')]: {
    padding: 3,
  },
}));
```

#### Using with sx prop

```typescript
<Box sx={{  border: 1 }} />
// equivalent to border: '1px solid black'

<Box sx={{ bgcolor: 'primary' }} />
// equivalent to backgroundColor: theme => theme.vars.palette.primary

<Box sx={{ margin: 2 }} />
// equivalent to margin: calc(2 * var(--nova-spacing, 4px))
```

#### Using CSS Variables directly

All design tokens are available as CSS custom properties:

```css
.my-component {
  /* Color tokens */
  background-color: var(--nova-palette-primary);
  color: var(--nova-palette-onPrimary);
  border-color: var(--nova-palette-outline);
  
  /* Size tokens */
  padding: var(--space-between-horizontal-2xs);
  border-radius: var(--radius-md);
}
```

#### Using Primitive Colors (Not Recommended)

You can access primitive colors if needed, but it's not recommended as they don't follow the Material Design 3 semantic color system:

```typescript
import { primitives } from '@hxnova/themes';

// Not recommended - bypasses semantic color roles
const myComponent = {
  backgroundColor: primitives.colors.nova.brandBlue[500],
  color: primitives.nova.colors.nova.neutral[100],
};
```

**Note**: Always prefer using semantic color tokens (primary, surface, outline, etc.) over primitive colors for better theme consistency and accessibility.


#### Using Breakpoints with useMediaQuery

Use the `useMediaQuery` hook with Nova's breakpoint system for responsive design:

##### Available Breakpoints
- `xs`: 0px (Mobile)
- `sm`: 600px (Tablet Portrait)
- `md`: 840px (Tablet Landscape)
- `lg`: 1200px (Laptop Display)
- `xl`: 1600px (Desktop Display)

##### Example Usage
```typescript
import { useMediaQuery } from '@hxnova/react-components';
import { NovaTheme } from '@hxnova/themes';

function ResponsiveComponent() {
  const isMobile = useMediaQuery(NovaTheme.breakpoints.down('sm'));
  const isTablet = useMediaQuery(NovaTheme.breakpoints.between('sm', 'md'));
  const isDesktop = useMediaQuery(NovaTheme.breakpoints.up('lg'));

  return (
    <div>
      {isMobile && <MobileLayout />}
      {isTablet && <TabletLayout />}
      {isDesktop && <DesktopLayout />}
    </div>
  );
}
```

## Next.js Integration

### Configuring the theme

Next.js requires additional configuration for server-side rendering (SSR) support:

**1. Install required dependencies:**

```bash
npm install @emotion/cache @emotion/react @emotion/styled
```

**2. Import stylesheets in your global CSS file (`app/globals.css`):**

```css
@import '@hxnova/themes/styles.css';
@import '@hxnova/icons/icons.css';

/* Your other global styles */
```

**3. Add an AppCacheProvider to make emotion-styled Nova components work with the Next.js project:**

```typescript
'use client';

import * as React from 'react';
import createCache, { EmotionCache } from '@emotion/cache';
import { CacheProvider as DefaultCacheProvider } from '@emotion/react';
import { useServerInsertedHTML } from 'next/navigation';

export type AppCacheProviderProps = {
  /**
   * By default <CacheProvider /> from 'import { CacheProvider } from "@emotion/react"'.
   */
  CacheProvider?: React.ElementType<{ value: EmotionCache }>;
  children: React.ReactNode;
};

/**
 * Emotion works OK without this provider but it's recommended to use this provider to improve performance.
 * Without it, Emotion will generate a new <style> tag during SSR for every component.
 * See https://github.com/mui/material-ui/issues/26561#issuecomment-855286153 for why it's a problem.
 */
export default function AppCacheProvider(props: AppCacheProviderProps) {
  const { CacheProvider = DefaultCacheProvider, children } = props;

  const [{ cache, flush }] = React.useState(() => {
    const cache = createCache({ key: 'nova' });
    cache.compat = true;

    const prevInsert = cache.insert;
    let inserted: { name: string; isGlobal: boolean }[] = [];
    // Override the insert method to support streaming SSR with flush().
    cache.insert = (...args) => {
      const [selector, serialized] = args;
      if (cache.inserted[serialized.name] === undefined) {
        inserted.push({
          name: serialized.name,
          isGlobal: !selector,
        });
      }
      return prevInsert(...args);
    };

    const flush = () => {
      const prevInserted = inserted;
      inserted = [];
      return prevInserted;
    };

    return { cache, flush };
  });
  useServerInsertedHTML(() => {
    const inserted = flush();
    if (inserted.length === 0) {
      return null;
    }
    let styles = '';
    let dataEmotionAttribute = cache.key;

    const globals: {
      name: string;
      style: string;
    }[] = [];

    inserted.forEach(({ name, isGlobal }) => {
      const style = cache.inserted[name];

      if (typeof style === 'string') {
        if (isGlobal) {
          globals.push({ name, style });
        } else {
          styles += style;
          dataEmotionAttribute += ` ${name}`;
        }
      }
    });

    return (
      <React.Fragment>
        {globals.map(({ name, style }) => (
          <style
            key={name}
            data-emotion={`${cache.key}-global ${name}`}
            // eslint-disable-next-line react/no-danger
            dangerouslySetInnerHTML={{ __html: style }}
          />
        ))}
        {styles && (
          <style
            data-emotion={dataEmotionAttribute}
            // eslint-disable-next-line react/no-danger
            dangerouslySetInnerHTML={{ __html: styles }}
          />
        )}
      </React.Fragment>
    );
  });

  return <CacheProvider value={cache}>{children}</CacheProvider>;
}
```

**4. Configure the layout (`app/layout.tsx`):**

```typescript
'use client';
import React from 'react';
import { CssBaseline } from '@hxnova/react-components/CssBaseline';
import { ThemeProvider } from '@hxnova/react-components/styles';
import novaTheme from '@hxnova/themes';
import './globals.css';
import AppCacheProvider from '@/components/AppCacheProvider';
import InitColorSchemeScript from '@hxnova/react-components/InitColorSchemeScript';

export default async function RootLayout(props: { children: React.ReactNode }) {
  return (
    <AppCacheProvider>
      <ThemeProvider theme={novaTheme}>
        <html lang="en">
          <CssBaseline />
          <body>
            <InitColorSchemeScript attribute="class" />
            {props.children}
          </body>
        </html>
      </ThemeProvider>
    </AppCacheProvider>
  );
}

```

### Support Light & Dark Theme

For Next.js, theme switching requires special handling due to SSR. Use the `useColorScheme` hook in client components:

```typescript
'use client'; 

import React from 'react';
import { useColorScheme } from '@hxnova/react-components/styles';
import { IconButton } from '@hxnova/react-components/IconButton';

function ColorSchemeToggleButton() {
  const { mode, setMode } = useColorScheme();

  const toggleColorScheme = () => {
    setMode(mode === 'dark' ? 'light' : 'dark');
  };

  return (
    <IconButton onClick={toggleColorScheme}>
      {mode === 'light' ? '🌙' : '🔆'}
    </IconButton>
  );
}
```