Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/admin-sdk-theme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopware-ag/meteor-admin-sdk": minor
---

Exposed the Administration color theme to iframe apps via `context.getTheme()` and `context.subscribeTheme()`, plus a `context.syncTheme()` helper that mirrors the resolved theme onto an element's `data-theme` attribute.
5 changes: 5 additions & 0 deletions .changeset/theme-select-color-scheme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopware-ag/meteor-component-library": minor
---

Added an `mt-theme-select` component for choosing the application color theme (light, dark, or system) and a `useColorScheme` composable that resolves the system preference, applies the resolved theme to `data-theme`, and persists the choice.
129 changes: 128 additions & 1 deletion docs/admin-sdk/api-reference/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ sidebar_position: 40

The Context API provides read access to the current state of the Shopware Administration. Extensions can use these methods to retrieve information about the active language, locale, currency, environment, Shopware version, and more.

This is useful for adapting extension behavior based on the current Administration context — for example, loading translations for the active language or checking the Shopware version before using a newer API.
This is useful for adapting extension behavior based on the current Administration context — for example, loading translations for the active language, matching the active color theme, or checking the Shopware version before using a newer API.

```ts
import { context } from "@shopware-ag/meteor-admin-sdk";
Expand Down Expand Up @@ -175,6 +175,133 @@ context.subscribeLocale(({ locale, fallbackLocale }) => {
}
```

## getTheme()

Returns the current resolved color theme of the Administration. A user's `system` preference is always resolved to the value that is actually applied, so this is either `"light"` or `"dark"`. Use this to render your iframe app in the same theme as the Administration.

#### Usage

```ts
const theme = await context.getTheme();
```

#### Parameters

No parameters needed.

#### Return value

```ts
Promise<"light" | "dark">
```

#### Example value

```ts
"dark";
```

## subscribeTheme()

Subscribes to theme changes in the Administration. The callback fires whenever the resolved theme changes, allowing your app to react immediately. Returns a function that stops the subscription.

#### Usage

```ts
const unsubscribe = context.subscribeTheme((theme) => {
// theme is "light" or "dark"
});

// Stop listening when you no longer need updates
unsubscribe();
```

#### Parameters

| Name | Description |
| :--------------- | :----------------------------------------- |
| `callbackMethod` | Called every time the resolved theme changes |

#### Callback value

```ts
"light" | "dark"
```

## syncTheme()

A small helper that mirrors the Administration theme onto your app. It writes the resolved theme to the `data-theme` attribute of an element (the document root by default), applies the current value immediately, and keeps it in sync with future changes. Because Meteor tokens are theme-aware through `data-theme`, this is usually all an iframe app needs to match the Administration's appearance.

#### Usage

```ts
// Sync to <html data-theme="…"> and keep it updated
const stopSync = await context.syncTheme();

// Stop syncing later (e.g. when the app is torn down)
stopSync();

// Sync to a specific element instead of the document root
await context.syncTheme({ target: document.getElementById("app") });
```

#### Parameters

| Name | Description |
| :-------------- | :----------------------------------------------------------------------------------- |
| `options.target` | Element whose `data-theme` attribute is updated. Defaults to `document.documentElement` |

#### Return value

```ts
Promise<() => void>
```

The returned function stops the subscription.

### Minimal iframe app example

A complete iframe app that adopts the Administration theme and reacts to changes:

```html
<!doctype html>
<html>
<head>
<!-- Load your Meteor tokens stylesheet so themed CSS variables are available -->
<link rel="stylesheet" href="/your-meteor-tokens.css" />
</head>
<body>
<h1>My themed app</h1>

<script type="module">
import { context } from "@shopware-ag/meteor-admin-sdk";

// Mirror the Administration theme onto <html data-theme="…"> and keep it
// in sync. From here on, your Meteor-token-based styles follow the
// Administration automatically.
const stopSync = await context.syncTheme();

// Optional: stop syncing when the app unmounts.
window.addEventListener("beforeunload", () => stopSync());
</script>
</body>
</html>
```

If you need the theme value yourself (for example to swap an illustration), combine `getTheme` and `subscribeTheme`:

```ts
import { context } from "@shopware-ag/meteor-admin-sdk";

let theme = await context.getTheme();
render(theme);

context.subscribeTheme((next) => {
theme = next;
render(theme);
});
```

## getCurrency()

Returns the system currency configured for the Shopware instance. Use this when displaying prices or working with monetary values.
Expand Down
38 changes: 38 additions & 0 deletions packages/admin-sdk/src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,35 @@ export const getAppInformation = createSender('contextAppInformation', {});
export const can = createACLHelper(getAppInformation);
export const getModuleInformation = createSender('contextModuleInformation', {});
export const getShopId = createSender('contextShopId', {});
export const getTheme = createSender('contextTheme', {});
export const subscribeTheme = createSubscriber('contextTheme');

/**
* Syncs the resolved Administration theme to the `data-theme` attribute of an
* element (the document root by default). The current theme is applied
* immediately and the attribute is updated whenever the Administration theme
* changes.
*
* Returns a function that stops the subscription.
*/
export async function syncTheme(
options: { target?: HTMLElement } = {},
): Promise<() => void> {
const target = options.target ?? document.documentElement;

const apply = (theme: contextTheme['responseType']): void => {
target.dataset.theme = theme;
};

/*
* Subscribe before fetching the current value so a theme change published
* while getTheme() is in flight cannot slip through unobserved.
*/
const unsubscribe = subscribeTheme(apply);
apply(await getTheme());

return unsubscribe;
}

/**
* Get the current content language
Expand Down Expand Up @@ -123,3 +152,12 @@ export type contextModuleInformation = {
export type contextShopId = {
responseType: string|null,
}

/**
* Get the current resolved Administration color theme. A "system" preference is
* always resolved to the actually applied value, so this is either "light" or
* "dark".
*/
export type contextTheme = {
responseType: 'light' | 'dark',
}
105 changes: 105 additions & 0 deletions packages/admin-sdk/src/context/theme.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import flushPromises from 'flush-promises';
import type { handle as handleType, publish as publishType } from '../channel';
import type { getTheme as getThemeType, subscribeTheme as subscribeThemeType, syncTheme as syncThemeType } from './index';

let handle: typeof handleType;
let publish: typeof publishType;
let getTheme: typeof getThemeType;
let subscribeTheme: typeof subscribeThemeType;
let syncTheme: typeof syncThemeType;

// Collect every listener/subscription so leaked handlers from a failing
// assertion can never bleed into the next test (all handlers share one window).
const cleanups: Array<() => void> = [];
const track = (remove: () => void): (() => void) => {
cleanups.push(remove);
return remove;
};

describe('context theme', () => {
beforeAll(async () => {
window.addEventListener('message', (event: MessageEvent) => {
if (event.origin === '') {
event.stopImmediatePropagation();
const eventWithOrigin: MessageEvent = new MessageEvent('message', {
data: event.data,
origin: window.location.href,
});
window.dispatchEvent(eventWithOrigin);
}
});

const channel = await import('../channel');
handle = channel.handle;
publish = channel.publish;

const context = await import('./index');
getTheme = context.getTheme;
subscribeTheme = context.subscribeTheme;
syncTheme = context.syncTheme;
});

afterEach(() => {
cleanups.splice(0).forEach((remove) => remove());
delete document.documentElement.dataset.theme;
});

it('resolves the current theme via getTheme', async () => {
track(handle('contextTheme', () => 'dark' as const));

const theme = await getTheme();
expect(theme).toBe('dark');
});

it('notifies subscribers when the theme changes', async () => {
const seen: Array<'light' | 'dark'> = [];
track(subscribeTheme((theme) => {
seen.push(theme);
}));

publish('contextTheme', 'dark');
await flushPromises();
expect(seen.at(-1)).toBe('dark');

publish('contextTheme', 'light');
await flushPromises();
expect(seen.at(-1)).toBe('light');
expect(seen).toHaveLength(2);
});

it('stops notifying after unsubscribing', async () => {
const seen: Array<'light' | 'dark'> = [];
const unsubscribe = subscribeTheme((theme) => {
seen.push(theme);
});

publish('contextTheme', 'dark');
await flushPromises();
expect(seen).toHaveLength(1);

unsubscribe();

publish('contextTheme', 'light');
await flushPromises();
expect(seen).toHaveLength(1);
});

it('syncs the resolved theme to the document root and tracks changes', async () => {
track(handle('contextTheme', () => 'dark' as const));

track(await syncTheme());
expect(document.documentElement.dataset.theme).toBe('dark');

publish('contextTheme', 'light');
await flushPromises();
expect(document.documentElement.dataset.theme).toBe('light');
});

it('syncs the theme to a custom target element', async () => {
track(handle('contextTheme', () => 'dark' as const));
const target = document.createElement('div');

track(await syncTheme({ target }));
expect(target.dataset.theme).toBe('dark');
});
});
2 changes: 2 additions & 0 deletions packages/admin-sdk/src/message-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
contextUserInformation,
contextUserTimezone,
contextShopId,
contextTheme,
} from './context';
import type { uiComponentSectionRenderer } from './ui/component-section/index';
import type { uiTabsAddTabItem } from './ui/tabs';
Expand Down Expand Up @@ -71,6 +72,7 @@ export interface ShopwareMessageTypes {
contextAppInformation: contextAppInformation,
contextModuleInformation: contextModuleInformation,
contextShopId: contextShopId,
contextTheme: contextTheme,
getPageTitle: getPageTitle,
uiComponentSectionRenderer: uiComponentSectionRenderer,
uiTabsAddTabItem: uiTabsAddTabItem,
Expand Down
Loading
Loading