diff --git a/README.md b/README.md index b71182c..227a5cd 100644 --- a/README.md +++ b/README.md @@ -14,63 +14,13 @@ Designed for compatibility with `@rdlabo/ionic-theme-ios27`, so one markup tree ## Installation -In an existing Ionic project: +Use the [setup guide](docs/setup.md) to install the package, import +`@rdlabo/ionic-theme-md3/styles/theme.css`, and spread `md3ThemeConfig` into your +existing Ionic initializer's options. Ionic `>=8.8.0 <10` is supported. Apps on older Ionic +versions can use `@rdlabo/ionic-theme-md3@1.0.2`. -```bash -npm install @rdlabo/ionic-theme-md3 -``` - -Note: **If you use @ionic/core@ < 8.8.0**, use @rdlabo/ionic-theme-md3@1.0.2. - -Import the theme in your project's main CSS file (e.g., `src/styles.scss`) **after Ionic's core styles**. This order allows the MD default font to apply while preserving your application's `--ion-font-family` override. - -```css -@import '@rdlabo/ionic-theme-md3/dist/css/default-variables.css'; -@import '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css'; -``` - -### Configure animations - -If you installed only the MD3 theme, configure its animation as follows. - -```ts -import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue -import { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3'; - -// Angular -provideIonicAngular({ - ... - navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation, -}); - -// React -setupIonicReact({ - ... - navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation, -}); - -// Vue -createApp(App) - .use(IonicVue, { - ... - navAnimation: isPlatform('ios') ? undefined: mdTransitionAnimation, -}) -``` - -### Check the theme - -Test on Android. When previewing on desktop, set Ionic mode to `md` in your existing framework initialization config (for example `mode: 'md'`). - -Use this markup to preview the inset grouped list look. For the list structure the theme expects, see [Using ion-item-group](https://docs.rdlabo.dev/projects/ionic-theme-md3/docs/using-ion-item-group). - -```html - - - Notifications - Appearance - - -``` +For existing apps, follow the [optional setup simplification](docs/migration.md#optional-simplify-the-setup). +For individual CSS files and animation settings, use [configuration](docs/configuration.md). ### Android Settings appearance @@ -86,69 +36,20 @@ for changes to toolbar, list and toggle dimensions. ### Optional: use the MD3 and iOS 27 themes together -Install the iOS 27 theme to style both Ionic modes from the same application. - -The current releases of both themes require `@ionic/core` 8.8.1 or later. Upgrade Ionic before using this setup if your application is on 8.8.0 or earlier. - -```bash -npm install @rdlabo/ionic-theme-ios27 -``` - -When your global stylesheet uses Sass, initialize the themes in this order: - -```scss -@use '@rdlabo/ionic-theme-ios27/src/styles/default-variables.scss' as ios27-vars; -@use '@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27.scss'; -@use '@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27-dark-class.scss'; -@use '@rdlabo/ionic-theme-ios27/src/styles/md-remove-ios-class-effect.scss'; -@use '@rdlabo/ionic-theme-md3/dist/css/default-variables.css' as md3-vars; -@use '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css'; -``` - -The example uses Ionic's class-based dark mode. Your global stylesheet must also load Ionic's matching dark palette, such as `@ionic/angular/css/palettes/dark.class.css` for Angular. When using `dark-system` or `dark-always`, select the same variant for both Ionic's palette and the iOS 27 theme. See Ionic's [Dark Mode documentation](https://ionicframework.com/docs/theming/dark-mode). The explicit `ios27-vars` and `md3-vars` namespaces prevent the two variable modules from using the same default namespace. - -Configure both transition implementations when both themes are installed: - -```ts -import { isPlatform } from '@ionic/core'; // or @ionic/angular (Ionic 9), @ionic/angular/standalone (Ionic 8), @ionic/react, @ionic/vue -import { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios27'; -import { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3'; - -// Angular -provideIonicAngular({ - ... - navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation, - popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined, - popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined, -}); - -// React -setupIonicReact({ - ... - navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation, - popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined, - popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined, -}); - -// Vue -createApp(App) - .use(IonicVue, { - ... - navAnimation: isPlatform('ios') ? iosTransitionAnimation : mdTransitionAnimation, - popoverEnter: isPlatform('ios') ? popoverEnterAnimation : undefined, - popoverLeave: isPlatform('ios') ? popoverLeaveAnimation : undefined, - }); -``` +Follow [combined theme configuration](docs/configuration.md#use-md3-and-ios-27-together) +for stylesheet ordering and platform-based animation selection. ## Documentation +- [Setup](docs/setup.md) — install and configure the theme with the theme stylesheet and configuration object. +- [Configuration](docs/configuration.md) — individual stylesheets, animations and existing mode selection. - [Android dynamic colors](docs/dynamic-colors.md) — opt-in Material You setup, light/dark appearance, lifecycle and role mapping. - [Color system](docs/color-system.md) — Ionic palette defaults, optional Material roles and component color assignments. - [Android Settings appearance](docs/android-settings.md) — default geometry, CSS font defaults and Ionic customization. - [Using ion-item-group](https://docs.rdlabo.dev/projects/ionic-theme-md3/docs/using-ion-item-group) — shared inset-list markup for iOS 27 and MD3. - [Special markup](https://docs.rdlabo.dev/projects/ionic-theme-md3/docs/special-markup) — opt-in component combinations used by the demo. - [ESLint](https://docs.rdlabo.dev/projects/ionic-theme-md3/docs/eslint) — check list structure with ESLint rules. -- [Migration](https://docs.rdlabo.dev/projects/ionic-theme-md3/docs/migration) — changes required when updating theme markup. +- [Migration](docs/migration.md) — optional setup simplification and version-specific upgrade steps. ## Related Projects diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..98d7e71 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,123 @@ +--- +title: Theme configuration +--- + +# Theme configuration + +The [setup guide](setup.md) uses the combined MD3 theme stylesheet and `md3ThemeConfig`. +The individual stylesheets and animation exports support custom composition. + +## Load individual stylesheets + +Place these imports after Ionic's core CSS and your chosen Ionic dark palette: + +```css +@import '@rdlabo/ionic-theme-md3/dist/css/default-variables.css'; +@import '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css'; +``` + +These two files form `styles/theme.css`. Use either the theme stylesheet or the individual +imports. Put application CSS overrides after the theme. + +Ionic owns dark-palette selection. Choose `dark.system.css`, `dark.class.css` or +`dark.always.css` from `@ionic/core/css/palettes/` according to your app's appearance +policy. [Dynamic Colors](dynamic-colors.md) manages `ion-palette-dark` and uses +`dark.class.css`. + +## Configure animations directly + +Use the MD3 transition for Ionic's Material Design mode in your existing initializer: + +```ts +import { isPlatform } from '@ionic/core'; +import { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3'; + +const ionicConfig = { + navAnimation: isPlatform('ios') ? undefined : mdTransitionAnimation, +}; +``` + +Pass this object to Angular's `provideIonicAngular(ionicConfig)`, React's +`setupIonicReact(ionicConfig)` or Vue's `app.use(IonicVue, ionicConfig)`. Angular +imports `provideIonicAngular` from `@ionic/angular` in Ionic 9 and from +`@ionic/angular/standalone` in Ionic 8. React imports `setupIonicReact` from +`@ionic/react`, and Vue imports `IonicVue` from `@ionic/vue`. + +[Combined theme configuration](#use-md3-and-ios-27-together) shows platform-based +transitions when both themes are installed. + +## Preserve existing mode selection + +`md3ThemeConfig` includes both `mode: 'md'` and the MD3 transition. Select its +configuration when the application selects Material Design mode: + +```ts +import { isPlatform } from '@ionic/core'; +import { md3ThemeConfig } from '@rdlabo/ionic-theme-md3'; + +const ionicConfig = { + ...(isPlatform('ios') ? { mode: 'ios' as const } : md3ThemeConfig), + // animated: false, +}; +``` + +The iOS branch uses Ionic's standard iOS appearance and animations. Options after +the spread override the selected defaults, including an explicit +`navAnimation: undefined` for Ionic's default transition. See +[migration](migration.md#optional-simplify-the-setup) for a before-and-after +example. + +## Use MD3 and iOS 27 together + +Install the iOS 27 theme to style both Ionic modes from the same application. + +The current releases of both themes require `@ionic/core` 8.8.1 or later. Upgrade Ionic before using this setup if your application is on 8.8.0 or earlier. + +```bash +npm install @rdlabo/ionic-theme-ios27 +``` + +When your global stylesheet uses Sass, initialize the themes in this order: + +```scss +@use '@rdlabo/ionic-theme-ios27/src/styles/default-variables.scss' as ios27-vars; +@use '@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27.scss'; +@use '@rdlabo/ionic-theme-ios27/src/styles/ionic-theme-ios27-dark-class.scss'; +@use '@rdlabo/ionic-theme-ios27/src/styles/md-remove-ios-class-effect.scss'; +@use '@rdlabo/ionic-theme-md3/dist/css/default-variables.css' as md3-vars; +@use '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css'; +``` + +The example uses Ionic's class-based dark mode. Your global stylesheet must also load Ionic's matching dark palette, such as `@ionic/angular/css/palettes/dark.class.css` for Angular. When using `dark-system` or `dark-always`, select the same variant for both Ionic's palette and the iOS 27 theme. See Ionic's [Dark Mode documentation](https://ionicframework.com/docs/theming/dark-mode). The explicit `ios27-vars` and `md3-vars` namespaces prevent the two variable modules from using the same default namespace. + +Configure both transition implementations when both themes are installed: + +```ts +import { isPlatform } from '@ionic/core'; +import { iosTransitionAnimation, popoverEnterAnimation, popoverLeaveAnimation } from '@rdlabo/ionic-theme-ios27'; +import { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3'; + +const ios = isPlatform('ios'); +const ionicConfig = { + // Add application options here. + navAnimation: ios ? iosTransitionAnimation : mdTransitionAnimation, + popoverEnter: ios ? popoverEnterAnimation : undefined, + popoverLeave: ios ? popoverLeaveAnimation : undefined, +}; +``` + +Pass this object to your existing framework initializer using the imports described +in [Configure animations directly](#configure-animations-directly). + +The exported theme configuration objects can also select both modes: + +```ts +import { isPlatform } from '@ionic/core'; +import { ios27ThemeConfig } from '@rdlabo/ionic-theme-ios27'; +import { md3ThemeConfig } from '@rdlabo/ionic-theme-md3'; + +const ionicConfig = { + ...(isPlatform('ios') ? ios27ThemeConfig : md3ThemeConfig), + // animated: false, +}; +``` diff --git a/docs/migration.md b/docs/migration.md index 22221cc..4a09846 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -6,7 +6,67 @@ title: Migration Review every section newer than the version currently installed, in ascending order. For example, when upgrading from 1.x to 9.0.0, complete the 2.0.0 migration steps before reviewing 9.0.0. -Each section lists only the changes that require application code or configuration updates. +Versioned sections list changes that require application code or configuration updates. The setup simplification below is optional. + +## Optional: simplify the setup + +The theme stylesheet and `md3ThemeConfig` provide a shorter setup. Existing individual +stylesheet paths and animation exports remain supported. Applications can adopt +the theme stylesheet and configuration object independently. + +### Replace the theme stylesheet imports + +```diff + @import '@ionic/core/css/palettes/dark.system.css'; +- @import '@rdlabo/ionic-theme-md3/dist/css/default-variables.css'; +- @import '@rdlabo/ionic-theme-md3/dist/css/ionic-theme-md3.css'; ++ @import '@rdlabo/ionic-theme-md3/styles/theme.css'; +``` + +Keep the Ionic dark palette your application already selects. The example uses +system appearance; class-controlled and always-dark applications keep their +corresponding palette. Keep Ionic's base styles before these imports and +application overrides after them. MD3 provides one theme stylesheet shared by these modes. + +### Replace animation wiring + +For an app already using Material Design mode, replace the manual setup as follows. + +```diff +- import { mdTransitionAnimation } from '@rdlabo/ionic-theme-md3'; ++ import { md3ThemeConfig } from '@rdlabo/ionic-theme-md3'; + +- const ionicConfig = { +- mode: 'md' as const, +- navAnimation: mdTransitionAnimation, +- }; ++ const ionicConfig = { ++ ...md3ThemeConfig, ++ // animated: false, ++ }; +``` + +Pass the object to your existing framework initializer. The theme configuration +sets `mode: 'md'` and the matching MD3 transition. For an application that chooses +its appearance by platform, choose the configuration for the selected mode: + +```ts +import { isPlatform } from '@ionic/core'; +import { md3ThemeConfig } from '@rdlabo/ionic-theme-md3'; + +const ionicConfig = { + ...(isPlatform('ios') ? { mode: 'ios' as const } : md3ThemeConfig), + // animated: false, +}; +``` + +The application owns mode selection. Retain an existing explicit mode by selecting +the matching theme configuration. Put application settings after the spread so +custom animations take precedence, including an explicit +`navAnimation: undefined` for Ionic's default transition. See [setup](setup.md) +for framework imports and [configuration](configuration.md) for individual APIs +and combined-theme selection. Dynamic Colors continues to use Ionic's +[class-based palette](dynamic-colors.md). ## Migrating to 9.2.0: Android Settings alignment diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..d0009e6 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,96 @@ +--- +title: Set up the MD3 theme +--- + +# Set up the MD3 theme + +Use an existing Ionic Angular, React or Vue app with Ionic `>=8.8.0 <10`. + +## Install the theme + +```sh +npm install @rdlabo/ionic-theme-md3 +``` + +## Load the theme stylesheet + +Add one import to your global stylesheet after Ionic's base CSS and your selected +Ionic dark palette: + +```css +@import '@rdlabo/ionic-theme-md3/styles/theme.css'; +``` + +This stylesheet combines the default variables and MD3 component styles. When adopting +it in an existing app, replace the individual MD3 stylesheet imports. Keep your +chosen Ionic dark-palette import, such as +`@ionic/core/css/palettes/dark.system.css`, and put application overrides after the +theme stylesheet. MD3 has one theme stylesheet for all appearance modes. For +[Dynamic Colors](dynamic-colors.md), select Ionic's `dark.class.css` palette. + +## Configure Ionic + +Spread `md3ThemeConfig` into the options passed to the Ionic initializer your app already calls, before +rendering components. Choose the example for your framework. + +### Angular + +```ts +import { provideIonicAngular } from '@ionic/angular'; // Ionic 9 +// Ionic 8: import { provideIonicAngular } from '@ionic/angular/standalone'; +import { md3ThemeConfig } from '@rdlabo/ionic-theme-md3'; + +provideIonicAngular({ + ...md3ThemeConfig, + // animated: false, +}); +``` + +### React + +```ts +import { setupIonicReact } from '@ionic/react'; +import { md3ThemeConfig } from '@rdlabo/ionic-theme-md3'; + +setupIonicReact({ + ...md3ThemeConfig, + // animated: false, +}); +``` + +### Vue + +```ts +import { IonicVue } from '@ionic/vue'; +import { md3ThemeConfig } from '@rdlabo/ionic-theme-md3'; + +app.use(IonicVue, { + ...md3ThemeConfig, + // animated: false, +}); +``` + +The configuration sets `mode: 'md'` and the MD3 page transition. Add application +options after the spread to override these defaults. For platform-dependent +appearance, select the configuration matching the mode as shown in +[configuration](configuration.md#preserve-existing-mode-selection). +The exported object supports imports during server rendering. + +## Preview a screen + +Start your app's usual development server. Open a page containing this markup, +using your framework's component syntax: + +```html + + + Notifications + Appearance + + +``` + +Check the grouped list, navigate between pages, and switch the device appearance +using your chosen Ionic dark-palette policy. See [list structure](using-ion-item-group.md) +for the expected grouped markup and [configuration](configuration.md) for individual +stylesheets, animations and existing platform-based mode selection. diff --git a/package.json b/package.json index 6c73d7d..5cda3c0 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "import": "./dist/index.js" }, "./dist/css/*": "./dist/css/*", + "./styles/theme.css": "./dist/styles/theme.css", "./src/styles/*": "./src/styles/*", "./dynamic-colors": { "types": "./dist/dynamic-colors.d.ts", @@ -33,14 +34,16 @@ "prebuild": "rm -rf dist", "build": "npm run build:css && npm run build:ts", "prebuild:css": "rdlabo-copy-structured-list src/styles/utils/structured-list.scss", - "build:css": "rm -rf dist/css && sass src/styles:dist/css --style=compressed --no-source-map", + "build:css": "rm -rf dist/css && sass src/styles:dist/css --style=compressed --no-source-map && npm run build:presets", + "build:presets": "node scripts/build-presets.mjs", "build:ts": "tsc --noEmit && rdlabo-build-theme", "build:demo": "npm run build && cd demo && npm install && npm run build -- --configuration=production", "lint": "prettier --check \"./**/*.{scss,ts}\" && prettier --parser angular --check \"./**/*.html\"", "fmt": "prettier --write \"./**/*.{scss,ts}\" && prettier --parser angular --write \"./**/*.html\"", "release": "np --no-tests --no-publish", "prepublishOnly": "npm run build", - "test:esm": "rdlabo-check-esm", + "test:esm": "rdlabo-check-esm && node --test scripts/presets.test.mjs", + "test:presets": "npm run build && node --test scripts/presets.test.mjs", "test:dynamic-colors": "npm --prefix demo run test:dynamic-colors" }, "repository": { diff --git a/scripts/build-presets.mjs b/scripts/build-presets.mjs new file mode 100644 index 0000000..81aa820 --- /dev/null +++ b/scripts/build-presets.mjs @@ -0,0 +1,10 @@ +import { mkdir, readFile, rm, writeFile } from 'node:fs/promises'; + +const theme = await Promise.all( + ['default-variables', 'ionic-theme-md3'].map((name) => readFile(new URL(`../dist/css/${name}.css`, import.meta.url), 'utf8')), +); +const output = new URL('../dist/styles/', import.meta.url); +await rm(new URL('../dist/preset/', import.meta.url), { recursive: true, force: true }); +await rm(output, { recursive: true, force: true }); +await mkdir(output, { recursive: true }); +await writeFile(new URL('theme.css', output), `${theme.join('\n')}\n`); diff --git a/scripts/presets.test.mjs b/scripts/presets.test.mjs new file mode 100644 index 0000000..e384455 --- /dev/null +++ b/scripts/presets.test.mjs @@ -0,0 +1,53 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { test } from 'node:test'; +import { md3ThemeConfig, mdTransitionAnimation } from '../dist/index.js'; + +test('MD3 configuration supports object spread and overrides during SSR', () => { + assert.equal(md3ThemeConfig.mode, 'md'); + assert.equal(md3ThemeConfig.navAnimation, mdTransitionAnimation); + const customAnimation = () => {}; + const config = { ...md3ThemeConfig, navAnimation: customAnimation, animated: false }; + assert.equal(config.navAnimation, customAnimation); + assert.equal(config.animated, false); + assert.equal(md3ThemeConfig.navAnimation, mdTransitionAnimation); + assert.equal({ ...md3ThemeConfig, navAnimation: undefined }.navAnimation, undefined); + assert.equal({ ...md3ThemeConfig, mode: undefined }.mode, undefined); +}); + +test('the MD3 stylesheet is ordered, exported and packed without stale variants', async () => { + const cache = await mkdtemp(join(tmpdir(), 'md3-preset-pack-')); + try { + await writeFile(new URL('../dist/styles/stale.css', import.meta.url), 'stale'); + await mkdir(new URL('../dist/preset/', import.meta.url), { recursive: true }); + await writeFile(new URL('../dist/preset/default.css', import.meta.url), 'legacy'); + execFileSync(process.execPath, ['scripts/build-presets.mjs'], { cwd: new URL('../', import.meta.url) }); + assert.deepEqual(await readdir(new URL('../dist/styles/', import.meta.url)), ['theme.css']); + const [packed] = JSON.parse( + execFileSync('npm', ['pack', '--dry-run', '--ignore-scripts', '--json', '--cache', cache], { + cwd: new URL('../', import.meta.url), + encoding: 'utf8', + }), + ); + assert.deepEqual( + packed.files.map(({ path }) => path).filter((path) => path.startsWith('dist/styles/') || path.startsWith('dist/preset/')), + ['dist/styles/theme.css'], + ); + const url = import.meta.resolve('@rdlabo/ionic-theme-md3/styles/theme.css'); + const css = await readFile(new URL(url), 'utf8'); + const expected = await Promise.all( + ['default-variables', 'ionic-theme-md3'].map((name) => readFile(new URL(`../dist/css/${name}.css`, import.meta.url), 'utf8')), + ); + assert.equal(css, `${expected.join('\n')}\n`); + assert(!css.includes('@import')); + assert(!css.includes('sourceMappingURL=')); + for (const mode of ['default', 'system', 'class', 'always']) { + assert.throws(() => import.meta.resolve(`@rdlabo/ionic-theme-md3/preset/${mode}.css`), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' }); + } + } finally { + await rm(cache, { recursive: true, force: true }); + } +}); diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..2bb8f84 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,8 @@ +import type { IonicConfig } from '@ionic/core/components'; +import { mdTransitionAnimation } from './transition/md.transition'; + +/** Spread MD3 defaults into your framework's Ionic initialization options. */ +export const md3ThemeConfig: IonicConfig = { + mode: 'md', + navAnimation: mdTransitionAnimation, +}; diff --git a/src/index.ts b/src/index.ts index 8dc6aaa..65724e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './transition/md.transition'; +export { md3ThemeConfig } from './config'; export { materialToIonicColors, materialToMd3Colors, materialColorRoles, enableDynamicColors } from './dynamic-colors'; export type { MaterialColorScheme,