- {props.iconset.split(',').map((icon, index) => (
-
})
+
+ {props.iconset.split(',').map((icon, index, array) => (
+
+
+ {index < array.length - 1 && (
+
+ +
+
+ )}
+
))}
)}
diff --git a/src/components/global/DocsCard/styles.module.scss b/src/components/global/DocsCard/styles.module.scss
index 8b3955f0fe9..a57e862b16c 100644
--- a/src/components/global/DocsCard/styles.module.scss
+++ b/src/components/global/DocsCard/styles.module.scss
@@ -116,20 +116,19 @@ docs-card[disabled]::after {
.Card-icon-row {
position: relative;
+ display: flex;
+ gap: 8px;
}
- .Card-iconset__container {
- position: relative;
- }
-
- .Card-iconset__container .Card-icon {
- position: absolute;
- opacity: 0;
- transition: 0.8s opacity;
- }
-
- .Card-iconset__container .Card-icon--active {
- opacity: 1;
+ .Card-plus-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 48px;
+ color: var(--docs-card-border-c);
+ font-size: 22px;
+ font-weight: 600;
}
.Card-ionicon {
diff --git a/src/components/global/DocsCards/cards.css b/src/components/global/DocsCards/cards.css
index eadaa0350b0..cee05f91c3e 100644
--- a/src/components/global/DocsCards/cards.css
+++ b/src/components/global/DocsCards/cards.css
@@ -2,7 +2,20 @@ docs-cards {
display: grid;
font-size: 12px;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
- grid-gap: 1.35rem;
+ gap: 1.35rem;
+}
+
+/*
+ * This prevents a single card from stretching the full width by
+ * adding an invisible pseudo-element as a second grid item. This
+ * creates a 2-column layout on larger screens (card takes 1 column)
+ * and collapses to full width on smaller screens. This is used on the
+ * Packages & CDN page by the JavaScript section.
+ */
+docs-cards:has(docs-card:only-child)::after {
+ content: '';
+ display: block;
+ visibility: hidden;
}
docs-cards > docs-card {
diff --git a/src/components/global/Playground/index.tsx b/src/components/global/Playground/index.tsx
index 9d7f033b445..3489ae7840b 100644
--- a/src/components/global/Playground/index.tsx
+++ b/src/components/global/Playground/index.tsx
@@ -153,7 +153,7 @@ export default function Playground({
* The major version of Ionic to use in the generated StackBlitz examples.
* This will also load assets for StackBlitz from the specified version directory.
*/
- version: number;
+ version: string;
}) {
if (!code || Object.keys(code).length === 0) {
console.warn('No code usage examples provided for this Playground example.');
diff --git a/src/components/global/Playground/stackblitz.utils.ts b/src/components/global/Playground/stackblitz.utils.ts
index cdbbc6d4a26..d04378f74f7 100644
--- a/src/components/global/Playground/stackblitz.utils.ts
+++ b/src/components/global/Playground/stackblitz.utils.ts
@@ -38,10 +38,10 @@ export interface EditorOptions {
*/
mode?: string;
- version?: number;
+ version?: string;
}
-const loadSourceFiles = async (files: string[], version: number) => {
+const loadSourceFiles = async (files: string[], version: string) => {
const versionDir = `v${version}`;
const sourceFiles = await Promise.all(files.map((f) => fetch(`/docs/code/stackblitz/${versionDir}/${f}`)));
return await Promise.all(sourceFiles.map((res) => res.text()));
@@ -54,6 +54,7 @@ const openHtmlEditor = async (code: string, options?: EditorOptions) => {
options?.includeIonContent ? 'html/index.withContent.html' : 'html/index.html',
'html/variables.css',
'html/package.json',
+ 'html/package-lock.json',
'html/tsconfig.json',
'html/vite.config.ts',
],
@@ -72,11 +73,12 @@ const openHtmlEditor = async (code: string, options?: EditorOptions) => {
const indexHtml = 'index.html';
const files = {
'package.json': JSON.stringify(package_json, null, 2),
+ 'package-lock.json': defaultFiles[4],
'index.ts': defaultFiles[0],
[indexHtml]: defaultFiles[1],
'theme/variables.css': defaultFiles[2],
- 'tsconfig.json': defaultFiles[4],
- 'vite.config.ts': defaultFiles[5],
+ 'tsconfig.json': defaultFiles[5],
+ 'vite.config.ts': defaultFiles[6],
...options?.files,
};
@@ -106,6 +108,7 @@ const openAngularEditor = async (code: string, options?: EditorOptions) => {
const defaultFiles = await loadSourceFiles(
[
'angular/package.json',
+ 'angular/package-lock.json',
'angular/angular.json',
'angular/tsconfig.json',
'angular/tsconfig.app.json',
@@ -136,26 +139,34 @@ const openAngularEditor = async (code: string, options?: EditorOptions) => {
const files = {
'package.json': JSON.stringify(package_json, null, 2),
- 'angular.json': defaultFiles[1],
- 'tsconfig.json': defaultFiles[2],
- 'tsconfig.app.json': defaultFiles[3],
- [main]: defaultFiles[4],
- 'src/index.html': defaultFiles[5],
+ 'package-lock.json': defaultFiles[1],
+ 'angular.json': defaultFiles[2],
+ 'tsconfig.json': defaultFiles[3],
+ 'tsconfig.app.json': defaultFiles[4],
+ [main]: defaultFiles[5],
+ 'src/index.html': defaultFiles[6],
'src/polyfills.ts': `import 'zone.js';`,
- 'src/app/app.routes.ts': defaultFiles[6],
- 'src/app/app.component.ts': defaultFiles[7],
- 'src/app/app.component.css': defaultFiles[8],
- 'src/app/app.component.html': defaultFiles[9],
- 'src/app/example.component.ts': defaultFiles[10],
+ 'src/app/app.routes.ts': defaultFiles[7],
+ 'src/app/app.component.ts': defaultFiles[8],
+ 'src/app/app.component.css': defaultFiles[9],
+ 'src/app/app.component.html': defaultFiles[10],
+ 'src/app/example.component.ts': defaultFiles[11],
'src/app/example.component.html': code,
'src/app/example.component.css': '',
- 'src/styles.css': defaultFiles[11],
- 'src/global.css': defaultFiles[12],
- 'src/theme/variables.css': defaultFiles[13],
+ 'src/styles.css': defaultFiles[12],
+ 'src/global.css': defaultFiles[13],
+ 'src/theme/variables.css': defaultFiles[14],
...options?.files,
};
- files[main] = files[main].replace('provideIonicAngular()', `provideIonicAngular({ mode: '${options?.mode}' })`);
+ if (options?.version === '6') {
+ files[main] = files[main].replace(
+ 'importProvidersFrom(IonicModule.forRoot({ }))',
+ `importProvidersFrom(IonicModule.forRoot({ mode: '${options?.mode}' }))`
+ );
+ } else {
+ files[main] = files[main].replace('provideIonicAngular()', `provideIonicAngular({ mode: '${options?.mode}' })`);
+ }
sdk.openProject({
template: 'node',
@@ -173,6 +184,7 @@ const openReactEditor = async (code: string, options?: EditorOptions) => {
'react/variables.css',
'react/tsconfig.json',
'react/package.json',
+ 'react/package-lock.json',
'react/index.html',
'react/vite.config.js',
'react/browserslistrc',
@@ -192,16 +204,17 @@ const openReactEditor = async (code: string, options?: EditorOptions) => {
const appTsx = 'src/App.tsx';
const files = {
- '.eslintrc.js': defaultFiles[8],
- '.browserslistrc': defaultFiles[7],
- 'vite.config.js': defaultFiles[6],
- 'index.html': defaultFiles[5],
+ '.eslintrc.js': defaultFiles[9],
+ '.browserslistrc': defaultFiles[8],
+ 'vite.config.js': defaultFiles[7],
+ 'index.html': defaultFiles[6],
'src/index.tsx': defaultFiles[0],
[appTsx]: defaultFiles[1],
'src/main.tsx': code,
'src/theme/variables.css': defaultFiles[2],
'tsconfig.json': defaultFiles[3],
'package.json': JSON.stringify(package_json, null, 2),
+ 'package-lock.json': defaultFiles[5],
...options?.files,
'.stackblitzrc': `{
"startCommand": "yarn run start"
@@ -222,6 +235,7 @@ const openVueEditor = async (code: string, options?: EditorOptions) => {
const defaultFiles = await loadSourceFiles(
[
'vue/package.json',
+ 'vue/package-lock.json',
'vue/index.html',
'vue/variables.css',
'vue/vite.config.ts',
@@ -244,15 +258,16 @@ const openVueEditor = async (code: string, options?: EditorOptions) => {
const mainTs = 'src/main.ts';
const files = {
- 'src/App.vue': defaultFiles[5],
+ 'src/App.vue': defaultFiles[6],
'src/components/Example.vue': code,
- [mainTs]: defaultFiles[4],
- 'src/theme/variables.css': defaultFiles[2],
- 'index.html': defaultFiles[1],
- 'vite.config.ts': defaultFiles[3],
+ [mainTs]: defaultFiles[5],
+ 'src/theme/variables.css': defaultFiles[3],
+ 'index.html': defaultFiles[2],
+ 'vite.config.ts': defaultFiles[4],
'package.json': JSON.stringify(package_json, null, 2),
- 'tsconfig.json': defaultFiles[6],
- 'tsconfig.node.json': defaultFiles[7],
+ 'package-lock.json': defaultFiles[1],
+ 'tsconfig.json': defaultFiles[7],
+ 'tsconfig.node.json': defaultFiles[8],
...options?.files,
'.stackblitzrc': `{
"startCommand": "yarn run dev"
diff --git a/src/components/page/reference/ReleaseNotes/release-notes.json b/src/components/page/reference/ReleaseNotes/release-notes.json
index b47cef9f991..b0ef4c46e6b 100644
--- a/src/components/page/reference/ReleaseNotes/release-notes.json
+++ b/src/components/page/reference/ReleaseNotes/release-notes.json
@@ -1,4 +1,44 @@
[
+ {
+ "body": "
Bug Fixes
\n
\n- checkbox, toggle, radio-group: improve screen reader announcement timing for validation errors (#30714) (92db364)
\n
\n",
+ "name": "v8.7.10",
+ "published_at": "November 20 2025",
+ "tag_name": "v8.7.10",
+ "type": "patch",
+ "version": "8.7.10"
+ },
+ {
+ "body": "
Bug Fixes
\n
\n",
+ "name": "v8.7.9",
+ "published_at": "November 6 2025",
+ "tag_name": "v8.7.9",
+ "type": "patch",
+ "version": "8.7.9"
+ },
+ {
+ "body": "
Bug Fixes
\n
\n- checkbox, toggle: fire ionFocus and ionBlur (#30733) (54a1c86)
\n
\n",
+ "name": "v8.7.8",
+ "published_at": "October 30 2025",
+ "tag_name": "v8.7.8",
+ "type": "patch",
+ "version": "8.7.8"
+ },
+ {
+ "body": "
Bug Fixes
\n
\n- header: ensure one banner role in condensed header (#30718) (12084af)
\n- header: prevent flickering during iOS page transitions (#30705) (820fa28), closes #25326
\n- select: improve screen reader announcement timing for validation errors (#30723) (03303d7)
\n
\n",
+ "name": "v8.7.7",
+ "published_at": "October 16 2025",
+ "tag_name": "v8.7.7",
+ "type": "patch",
+ "version": "8.7.7"
+ },
+ {
+ "body": "
Bug Fixes
\n
\n",
+ "name": "v8.7.6",
+ "published_at": "October 9 2025",
+ "tag_name": "v8.7.6",
+ "type": "patch",
+ "version": "8.7.6"
+ },
{
"body": "
Bug Fixes
\n
\n",
"name": "v8.7.5",
@@ -198,45 +238,5 @@
"tag_name": "v8.4.6",
"type": "patch",
"version": "8.4.6"
- },
- {
- "body": "
Bug Fixes
\n
\n",
- "name": "v8.4.5",
- "published_at": "March 14 2025",
- "tag_name": "v8.4.5",
- "type": "patch",
- "version": "8.4.5"
- },
- {
- "body": "
Bug Fixes
\n
\n",
- "name": "v8.4.4",
- "published_at": "March 14 2025",
- "tag_name": "v8.4.4",
- "type": "patch",
- "version": "8.4.4"
- },
- {
- "body": "
Bug Fixes
\n
\n",
- "name": "v8.4.3",
- "published_at": "January 30 2025",
- "tag_name": "v8.4.3",
- "type": "patch",
- "version": "8.4.3"
- },
- {
- "body": "
Bug Fixes
\n
\n- segment: add logic to connect to segment-view in
componentDidLoad() callback (#30060) (000f553), closes #30000 \n- select-modal: match radio styles to iOS native (#30119) (3f8346e)
\n
\n",
- "name": "v8.4.2",
- "published_at": "January 23 2025",
- "tag_name": "v8.4.2",
- "type": "patch",
- "version": "8.4.2"
- },
- {
- "body": "
Bug Fixes
\n
\n- header: use aria attributes to hide small title when collapsed (#30027) (23763ab), closes #29347
\n- menu: hide from screen readers while animating (#30036) (845071c)
\n- overlays: announce info after opening based on platform (#30025) (f6188c4)
\n- overlays: focus management with checkbox/radio (#30026) (8ee42bb)
\n- toast: swipe gesture works with custom container layout (#29999) (470decc), closes #29998
\n
\n",
- "name": "v8.4.1",
- "published_at": "November 28 2024",
- "tag_name": "v8.4.1",
- "type": "patch",
- "version": "8.4.1"
}
]
diff --git a/src/styles/components/_admonition.scss b/src/styles/components/_admonition.scss
index cb4e1df485f..df575bd60b8 100644
--- a/src/styles/components/_admonition.scss
+++ b/src/styles/components/_admonition.scss
@@ -1,11 +1,13 @@
html[data-theme='light'] {
--admonition-note-c-bg: var(--c-yellow-10);
+ --admonition-important-c-bg: var(--c-purple-0);
--admonition-info-c-bg: var(--c-blue-0);
--admonition-tip-c-bg: var(--c-green-10);
--admonition-warning-c-bg: var(--c-orange-10);
--admonition-danger-c-bg: var(--c-red-0);
--admonition-code-note-c-bg: var(--c-yellow-30);
+ --admonition-code-important-c-bg: var(--c-purple-10);
--admonition-code-info-c-bg: var(--c-blue-10);
--admonition-code-tip-c-bg: var(--c-green-20);
--admonition-code-warning-c-bg: var(--c-orange-20);
@@ -14,12 +16,14 @@ html[data-theme='light'] {
html[data-theme='dark'] {
--admonition-note-c-bg: #241800;
+ --admonition-important-c-bg: #0d0024;
--admonition-info-c-bg: #000d24;
--admonition-tip-c-bg: #00240a;
--admonition-warning-c-bg: #240b00;
--admonition-danger-c-bg: #240002;
--admonition-code-note-c-bg: #3d2900;
+ --admonition-code-important-c-bg: #16003d;
--admonition-code-info-c-bg: #00163d;
--admonition-code-tip-c-bg: #003d11;
--admonition-code-warning-c-bg: #3d1200;
@@ -28,12 +32,14 @@ html[data-theme='dark'] {
:root {
--admonition-bar-note-c-bg: var(--c-yellow-80);
+ --admonition-bar-important-c-bg: var(--c-purple-80);
--admonition-bar-info-c-bg: var(--c-blue-80);
--admonition-bar-tip-c-bg: var(--c-green-80);
--admonition-bar-warning-c-bg: var(--c-orange-80);
--admonition-bar-danger-c-bg: var(--c-red-60);
--admonition-link-note-c: var(--c-yellow-90);
+ --admonition-link-important-c: var(--c-purple-90);
--admonition-link-info-c: var(--c-blue-90);
--admonition-link-tip-c: var(--c-green-90);
--admonition-link-warning-c: var(--c-orange-90);
@@ -89,6 +95,12 @@ html[data-theme='dark'] {
--admonition-code-c-bg: var(--admonition-code-note-c-bg);
--admonition-link-c: var(--admonition-link-note-c);
}
+ &-important {
+ --ifm-alert-background-color: var(--admonition-important-c-bg);
+ --admonition-bar-c-bg: var(--admonition-bar-important-c-bg);
+ --admonition-code-c-bg: var(--admonition-code-important-c-bg);
+ --admonition-link-c: var(--admonition-link-important-c);
+ }
&-info {
--ifm-alert-background-color: var(--admonition-info-c-bg);
--admonition-bar-c-bg: var(--admonition-bar-info-c-bg);
diff --git a/src/styles/components/_code.scss b/src/styles/components/_code.scss
index 27dc2a1b0d3..2bb2fc06cdc 100644
--- a/src/styles/components/_code.scss
+++ b/src/styles/components/_code.scss
@@ -51,6 +51,8 @@ code {
// Overrides
#__docusaurus {
[class*='codeBlockContainer_'] {
+ --prism-background-color: var(--code-block-bg-c);
+
box-shadow: none;
}
}
@@ -63,7 +65,6 @@ code {
code[class*='language-'],
pre[class*='language-'] {
- background: var(--code-block-bg-c);
white-space: pre;
word-spacing: normal;
word-break: normal;
@@ -121,7 +122,8 @@ pre[class*='language-'] {
.token.tag.punctuation,
.token.tag.class-name,
-.token.tag.punctuation + .token.tag:not(.attr-value):not([class*='language-']) {
+.token.tag.punctuation + .token.tag:not(.attr-value):not([class*='language-']),
+.token.tag.punctuation + .token.tag.language-vue:not(.attr-value) {
color: #2b90ff;
}
diff --git a/src/theme/prism-include-languages.ts b/src/theme/prism-include-languages.ts
new file mode 100644
index 00000000000..032dfd597e3
--- /dev/null
+++ b/src/theme/prism-include-languages.ts
@@ -0,0 +1,49 @@
+/**
+ * Prism is used to syntax highlight code blocks in markdown files.
+ *
+ * Original source:
+ * @link https://github.com/facebook/docusaurus/blob/main/packages/docusaurus-theme-classic/src/theme/prism-include-languages.ts
+ *
+ * Reason for overriding:
+ * - Add Vue language support since it's not included in Prism
+ */
+
+import siteConfig from '@generated/docusaurus.config';
+import type * as PrismNamespace from 'prismjs';
+import type { Optional } from 'utility-types';
+
+export default function prismIncludeLanguages(PrismObject: typeof PrismNamespace): void {
+ const {
+ themeConfig: { prism },
+ } = siteConfig;
+ const { additionalLanguages } = prism as { additionalLanguages: string[] };
+
+ // Prism components work on the Prism instance on the window, while prism-
+ // react-renderer uses its own Prism instance. We temporarily mount the
+ // instance onto window, import components to enhance it, then remove it to
+ // avoid polluting global namespace.
+ // You can mutate PrismObject: registering plugins, deleting languages... As
+ // long as you don't re-assign it
+
+ const PrismBefore = globalThis.Prism;
+ globalThis.Prism = PrismObject;
+
+ additionalLanguages.forEach((lang) => {
+ if (lang === 'php') {
+ // eslint-disable-next-line global-require
+ require('prismjs/components/prism-markup-templating.js');
+ }
+ // eslint-disable-next-line global-require, import/no-dynamic-require
+ require(`prismjs/components/prism-${lang}`);
+ });
+
+ // CUSTOM CODE
+ require('../theme/prism-languages/prism-vue');
+ // CUSTOM CODE END
+
+ // Clean up and eventually restore former globalThis.Prism object (if any)
+ delete (globalThis as Optional
).Prism;
+ if (typeof PrismBefore !== 'undefined') {
+ globalThis.Prism = PrismObject;
+ }
+}
diff --git a/src/theme/prism-languages/prism-vue.ts b/src/theme/prism-languages/prism-vue.ts
new file mode 100644
index 00000000000..36e7703f152
--- /dev/null
+++ b/src/theme/prism-languages/prism-vue.ts
@@ -0,0 +1,56 @@
+(function (Prism) {
+ Prism.languages.vue = Prism.languages.extend('markup', {
+ // Add Vue template interpolation (e.g., {{ expression }})
+ interpolation: {
+ pattern: /\{\{[^}]+\}\}/,
+ inside: {
+ punctuation: /\{\{|\}\}/,
+ expression: {
+ pattern: /[\s\S]+/,
+ inside: Prism.languages.javascript,
+ },
+ },
+ },
+
+ // Add Vue-specific attributes (v-bind, @click, :prop)
+ attribute: {
+ pattern: /(^|["'\s])(?:v-|:|\@|#)[\w-]+(?:\.[\w-]+)*(?=[^\w-])(?=[^=>]*=)/,
+ lookbehind: true,
+ alias: 'keyword',
+ },
+ });
+
+ Prism.languages.insertBefore('vue', 'comment', {
+ // Add support for