Skip to content
Merged
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
651 changes: 583 additions & 68 deletions demo/package-lock.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
"homepage": "https://ionicframework.com/",
"scripts": {
"ng": "ng",
"docs:generate": "node scripts/generate-docs.mjs",
"prestart": "npm run docs:generate",
"start": "ng serve",
"prebuild": "npm run docs:generate",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"pretest": "npm run docs:generate",
"test": "ng test",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
Expand Down Expand Up @@ -56,6 +60,8 @@
"eslint-plugin-jsdoc": "^48.2.1",
"eslint-plugin-prefer-arrow": "1.2.2",
"jsdom": "^26.0.0",
"marked": "^16.4.2",
"shiki": "^3.23.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.61.1",
"vitest": "^4.0.0"
Expand Down
45 changes: 45 additions & 0 deletions demo/scripts/generate-docs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { readFile, writeFile } from 'node:fs/promises';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

import { marked } from 'marked';
import { format } from 'prettier';
import { createHighlighter } from 'shiki';

const demoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const sourcePath = resolve(demoRoot, '../docs/special-markup.md');
const outputPath = resolve(demoRoot, 'src/app/docs/docs-content.generated.ts');

const markdown = await readFile(sourcePath, 'utf8');
const markdownBody = markdown.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
const markdownWithPreviews = markdownBody.replace(
/```html preview\r?\n([\s\S]*?)```/g,
(_match, code) =>
`\`\`\`html\n${code}\`\`\`\n\n<figure class="docs-example">\n<figcaption>Preview</figcaption>\n${code.trim()}\n</figure>`,
);
const highlighter = await createHighlighter({
themes: ['github-dark'],
langs: ['html', 'css'],
});
const highlightedBlocks = [];
const markdownWithCodePlaceholders = markdownWithPreviews.replace(/```([\w-]+)?\r?\n([\s\S]*?)```/g, (_match, language = 'text', code) => {
const highlighted = highlighter.codeToHtml(code.replace(/\r?\n$/, ''), {
lang: highlighter.getLoadedLanguages().includes(language) ? language : 'text',
theme: 'github-dark',
colorReplacements: {
'#24292e': '#151e2c',
'#6a737d': '#a0aab5',
},
});
const index = highlightedBlocks.push(`<div class="code-block-container">${highlighted}</div>`) - 1;
return `<div data-docs-code-block="${index}"></div>`;
});
let html = await marked.parse(markdownWithCodePlaceholders, { gfm: true });
html = html.replace(/<div data-docs-code-block="(\d+)"><\/div>/g, (_match, index) => highlightedBlocks[Number(index)]);
highlighter.dispose();
const generatedSource = await format(
`// Generated from docs/special-markup.md. Do not edit directly.\nexport const docsContentHtml = ${JSON.stringify(html)};\n`,
{ parser: 'typescript', printWidth: 140 },
);

await writeFile(outputPath, generatedSource);
3 changes: 3 additions & 0 deletions demo/src/app/docs/docs-content.generated.ts

Large diffs are not rendered by default.

90 changes: 4 additions & 86 deletions demo/src/app/docs/docs-page.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,94 +2,12 @@
<ion-toolbar>
<ion-title>Docs</ion-title>
<ion-buttons slot="end">
<ion-button fill="default" href="https://github.com/rdlabo-dev/ionic-theme-ios26"
><ion-icon name="logo-github" slot="icon-only"></ion-icon
></ion-button>
<ion-button fill="default" href="https://github.com/rdlabo-dev/ionic-theme-ios26">
<ion-icon name="logo-github" slot="icon-only"></ion-icon>
</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content [fullscreen]="true">
<section class="ion-padding-start ion-padding-end ion-padding-bottom">
<p>This is a demo of the iOS26 theme for Ionic Framework. Just import the CSS and start using it immediately.</p>
<code class="code-block">% npm install &#64;rdlabo/ionic-theme-ios26 --save</code>
<p>For detailed usage instructions, please refer to the README in the GitHub repository.</p>
</section>

<ion-list [inset]="true">
<ion-list-header><ion-label>Special Coding</ion-label><ion-note>Mode more iOS26</ion-note></ion-list-header>
<ion-item-group class="ion-margin-bottom">
<ion-item>
<div>
<h3><code>ion-button[fill=solid][type=submit]</code></h3>
<p>
Set color-brightness as a CSS variable. If using a `primary` button, the following is required. Please set your own color code.
</p>
<code class="code-block">--ion-color-primary-brightness: #96FEFF;</code>
<p style="display: flex; align-items: center">
<ion-button type="submit" color="primary" size="default">Submit</ion-button>
<ion-button type="submit" color="primary" size="default"
><ion-icon name="checkmark-outline" slot="icon-only"></ion-icon
></ion-button>
</p>
</div>
</ion-item>
</ion-item-group>

<ion-item-group>
<ion-item>
<div class="ion-padding-bottom">
<h3><code>ion-list[inset] ion-label + ion-note</code></h3>
<p>
When an inset list item contains an <code>ion-label</code> and an <code>ion-note</code> without slots, they are displayed
vertically as a two-line item.
</p>
<code class="code-block">
&lt;ion-list [inset]="true"&gt;<br />
&nbsp;&nbsp;&lt;ion-item&gt;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&lt;ion-label&gt;Network &amp; internet&lt;/ion-label&gt;<br />
&nbsp;&nbsp;&nbsp;&nbsp;&lt;ion-note&gt;Mobile, Wi-Fi, hotspot&lt;/ion-note&gt;<br />
&nbsp;&nbsp;&lt;/ion-item&gt;<br />
&lt;/ion-list&gt;
</code>
<ion-list [inset]="true">
<ion-item color="light">
<ion-label>Network & internet</ion-label>
<ion-note>Mobile, Wi-Fi, hotspot</ion-note>
</ion-item>
</ion-list>
</div>
</ion-item>
</ion-item-group>

<ion-item-group>
<ion-item>
<div class="ion-padding-bottom">
<h3><code>ion-segment.segment-expand</code></h3>
<p>To make segments span the full width, you need to apply a special class.</p>
<code class="code-block ion-margin-bottom">&lt;ion-segment class="segment-expand"&gt;&lt;/ion-segment&gt;</code>
<ion-segment value="default" class="segment-expand">
<ion-segment-button value="default">
<ion-label>New</ion-label>
</ion-segment-button>
<ion-segment-button value="segment">
<ion-label>Replied</ion-label>
</ion-segment-button>
</ion-segment>
</div>
</ion-item>
</ion-item-group>

<ion-item-group>
<ion-item>
<div class="ion-padding-bottom">
<h3><code>ion-searchbar.searchbar-classic</code></h3>
<p>iOS26's searchbar classic style.</p>
<code class="code-block ion-margin-bottom">&lt;ion-searchbar class="searchbar-classic"&gt;&lt;/ion-searchbar&gt;</code>
<ion-toolbar>
<ion-searchbar class="searchbar-classic"></ion-searchbar>
</ion-toolbar>
</div>
</ion-item>
</ion-item-group>
</ion-list>
<article class="docs-content" [innerHTML]="docsContentHtml | safeHtml"></article>
</ion-content>
16 changes: 5 additions & 11 deletions demo/src/app/docs/docs-page.component.scss
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
h3 {
font-size: 1.1rem;
font-weight: bold;
}

code.code-block {
background: var(--ion-color-dark);
color: var(--ion-color-dark-contrast);
display: inline-block;
padding: 8px 12px;
border-radius: 8px;
.docs-content {
width: 100%;
max-width: 800px;
margin: 0 auto;
padding: 24px 16px 72px;
}
12 changes: 12 additions & 0 deletions demo/src/app/docs/docs-page.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,16 @@ describe('DocsPage', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('renders the generated Markdown documentation', () => {
const element = fixture.nativeElement as HTMLElement;

expect(element.querySelector('article.docs-content h1')?.textContent).toContain('Special markup and classes');
expect(element.querySelectorAll('.code-block-container pre.shiki code').length).toBeGreaterThan(0);
expect(element.querySelector('.code-block-container code span[style*="color"]')).not.toBeNull();
expect(element.querySelectorAll('figure.docs-example').length).toBe(7);
const classicSearchPreview = element.querySelector('figure.docs-example > .ion-page');
expect(classicSearchPreview?.querySelector('ion-content ion-list[inset="true"] > ion-item-group > ion-item')).not.toBeNull();
expect(classicSearchPreview?.textContent).toContain('Recent item 10');
});
});
49 changes: 6 additions & 43 deletions demo/src/app/docs/docs-page.component.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,14 @@
import { Component, inject } from '@angular/core';

import { FormsModule } from '@angular/forms';
import {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonItem,
IonItemGroup,
IonLabel,
IonList,
IonListHeader,
IonNote,
IonSearchbar,
IonSegment,
IonSegmentButton,
IonTitle,
IonToolbar,
Platform,
} from '@demo/ionic';
import { Component } from '@angular/core';
import { IonButton, IonButtons, IonContent, IonHeader, IonIcon, IonTitle, IonToolbar } from '@demo/ionic';
import { docsContentHtml } from './docs-content.generated';
import { SafeHtmlPipe } from './safe-html.pipe';

@Component({
selector: 'app-docs-page',
templateUrl: './docs-page.component.html',
styleUrls: ['./docs-page.component.scss'],
imports: [
IonContent,
IonHeader,
IonTitle,
IonToolbar,
FormsModule,
IonIcon,
IonButton,
IonButtons,
IonList,
IonItem,
IonSegment,
IonLabel,
IonSegmentButton,
IonListHeader,
IonLabel,
IonNote,
IonItemGroup,
IonSearchbar,
],
imports: [IonContent, IonHeader, IonTitle, IonToolbar, IonIcon, IonButton, IonButtons, SafeHtmlPipe],
})
export class DocsPage {
readonly platform = inject(Platform);
readonly docsContentHtml = docsContentHtml;
}
13 changes: 13 additions & 0 deletions demo/src/app/docs/safe-html.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Pipe, PipeTransform, inject } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';

@Pipe({
name: 'safeHtml',
})
export class SafeHtmlPipe implements PipeTransform {
readonly #sanitizer = inject(DomSanitizer);

transform(html: string): SafeHtml {
return this.#sanitizer.bypassSecurityTrustHtml(html);
}
}
104 changes: 104 additions & 0 deletions demo/src/global.scss
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,110 @@ code {
font-family: Menlo, Consolas, monospace;
}

.docs-content {
line-height: 1.7;

h1,
h2 {
padding-bottom: 8px;
border-bottom: 1px solid var(--ion-color-step-150, #d7d8da);
}

h1 {
margin: 0 0 24px;
font-size: 1.75rem;
}

h2 {
margin: 40px 0 16px;
font-size: 1.4rem;
}

p {
margin: 16px 0;
}

a {
color: var(--ion-color-primary);
}

:not(pre) > code {
padding: 2px 6px;
border-radius: 4px;
background: var(--ion-color-step-100, #f1f3f5);
font-size: 0.85em;
}

.code-block-container {
margin: 16px 0;
overflow-x: auto;
border-radius: 8px;

pre.shiki {
margin: 0;
padding: 0;
line-height: 1.5;

code {
display: block;
padding: 16px;
overflow-x: auto;
background: transparent;
font-size: 0.85rem;
white-space: pre;
}
}
}
}

.docs-content .docs-example {
margin: 16px 0 32px;
padding: 16px;
overflow: hidden;
border: 1px solid var(--ion-color-step-150, #d7d8da);
border-radius: 16px;
background: var(--ion-background-color, #fff);

figcaption {
margin-bottom: 12px;
color: var(--ion-color-medium, #92949c);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}

ion-toolbar {
margin: 0;
}

> .ion-page {
position: relative;
height: 420px;
overflow: hidden;
border-radius: 12px;
contain: layout paint;

> ion-content {
--padding-start: 0 !important;
--padding-end: 0 !important;
}

> ion-header,
> ion-content > ion-header {
ion-toolbar {
--padding-start: 4px !important;
--padding-end: 4px !important;

ion-title.title-default {
padding-right: 90px !important;
padding-left: 90px !important;
}
}
}
}
}

ion-content {
--ios26-content-box-shadow-rgb: var(--ion-color-base-rgb, var(--ion-background-color-rgb, 255, 255, 255));
}
Expand Down
Loading
Loading