Merge branch 'next' into ionic-colors-support
@@ -15,6 +15,7 @@ ignoreFiles:
|
||||
- src/themes/functions.string.scss
|
||||
- src/themes/native.theme.default.scss
|
||||
- src/css/themes/*.scss
|
||||
- scripts/tokens/*.css
|
||||
|
||||
indentation: 2
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ ion-app,prop,theme,"ios" | "md" | "ionic",undefined,false,false
|
||||
|
||||
ion-avatar,shadow
|
||||
ion-avatar,prop,mode,"ios" | "md",undefined,false,false
|
||||
ion-avatar,prop,size,"large" | "medium" | "small" | undefined,undefined,false,false
|
||||
ion-avatar,prop,size,"large" | "medium" | "small" | "xsmall" | undefined,undefined,false,false
|
||||
ion-avatar,prop,theme,"ios" | "md" | "ionic",undefined,false,false
|
||||
ion-avatar,css-prop,--border-radius,ionic
|
||||
ion-avatar,css-prop,--border-radius,ios
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
"build.css": "npm run css.sass && npm run css.minify",
|
||||
"build.debug": "npm run clean && stencil build --debug",
|
||||
"build.docs.json": "stencil build --docs-json dist/docs.json",
|
||||
"build.tokens": "node ./scripts/tokens/index.js && npm run lint.sass.fix && npm run prettier.tokens",
|
||||
"build.tokens": "node ./scripts/tokens/index.js && npm run lint.fix && npm run prettier.tokens",
|
||||
"clean": "node scripts/clean.js",
|
||||
"css.minify": "cleancss -O2 -o ./css/ionic.bundle.css ./css/ionic.bundle.css",
|
||||
"css.sass": "sass --embed-sources --style compressed src/css:./css",
|
||||
@@ -90,7 +90,7 @@
|
||||
"lint.ts.fix": "npm run eslint -- --fix",
|
||||
"prerender.e2e": "node scripts/testing/prerender.js",
|
||||
"prettier": "prettier \"./src/**/*.{html,ts,tsx,js,jsx,scss}\"",
|
||||
"prettier.tokens": "prettier \"./src/foundations/*.scss\" --write --cache",
|
||||
"prettier.tokens": "prettier \"./src/foundations/*.{scss, html}\" --write --cache",
|
||||
"start": "npm run build.css && stencil build --dev --watch --serve",
|
||||
"test": "npm run test.spec && npm run test.e2e",
|
||||
"test.spec": "stencil test --spec --max-workers=2",
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// - It is probably the most well-known and widely used Design Tokens tool. It has also been regularly maintained for a long time.
|
||||
// - It can easily scale to different necessities we might have in the future.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const StyleDictionary = require('style-dictionary');
|
||||
|
||||
const targetPath = './src/foundations/';
|
||||
@@ -138,6 +140,137 @@ StyleDictionary.registerFormat({
|
||||
},
|
||||
});
|
||||
|
||||
// Register the custom format to generate HTML
|
||||
// Load the HTML template
|
||||
const template = fs.readFileSync(path.join(__dirname, 'preview.template.html'), 'utf8');
|
||||
|
||||
StyleDictionary.registerFormat({
|
||||
name: 'html/tokens',
|
||||
formatter: function ({ dictionary }) {
|
||||
// Function to extract numerical value from token name
|
||||
const extractValue = (tokenName) => {
|
||||
const match = tokenName.match(/-([0-9]+)/);
|
||||
return match ? parseInt(match[1], 10) : Number.MAX_SAFE_INTEGER;
|
||||
};
|
||||
|
||||
let colorTokens = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Color</th>
|
||||
<th>Hex</th>
|
||||
<th>Token Name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
let fontSizeTokens = '';
|
||||
let boxShadowTokens = '';
|
||||
let borderSizeTokens = '';
|
||||
let borderRadiusTokens = '';
|
||||
let borderStyleTokens = '';
|
||||
let fontWeightTokens = '';
|
||||
let letterSpacingTokens = '';
|
||||
let spaceTokens = '';
|
||||
|
||||
// Collect border-radius and space tokens for separate sorting
|
||||
let borderRadiusTokenList = [];
|
||||
let spaceTokenList = [];
|
||||
|
||||
dictionary.allProperties.forEach((token) => {
|
||||
if (token.attributes.category === 'color') {
|
||||
colorTokens += `
|
||||
<tr>
|
||||
<td><div class="color-swatch" style="background-color: ${token.value};"></div></td>
|
||||
<td>${token.value}</td>
|
||||
<td>${token.name}</td>
|
||||
</tr>
|
||||
`;
|
||||
} else if (token.attributes.category === 'font-size') {
|
||||
fontSizeTokens += `
|
||||
<div class="font-size-token" style="font-size: ${token.value};">
|
||||
${token.name} (${token.value})
|
||||
</div>
|
||||
`;
|
||||
} else if (token.attributes.category.startsWith('Elevation')) {
|
||||
const cssShadow = token.value.map(generateShadowValue).join(', ');
|
||||
boxShadowTokens += `
|
||||
<div class="shadow-token" style="box-shadow: ${cssShadow};">
|
||||
${token.name}
|
||||
</div>
|
||||
`;
|
||||
} else if (token.attributes.category === 'border-size' || token.attributes.category === 'border-width') {
|
||||
borderSizeTokens += `
|
||||
<div class="border-token" style="border-width: ${token.value};">
|
||||
${token.name} (${token.value})
|
||||
</div>
|
||||
`;
|
||||
} else if (token.attributes.category === 'border-radius') {
|
||||
borderRadiusTokenList.push(token); // Collect border-radius tokens
|
||||
} else if (token.attributes.category === 'border-style') {
|
||||
borderStyleTokens += `
|
||||
<div class="border-token" style="border: 1px ${token.value} #000;">
|
||||
${token.name} (${token.value})
|
||||
</div>
|
||||
`;
|
||||
} else if (token.attributes.category === 'font-weight') {
|
||||
fontWeightTokens += `
|
||||
<div class="weight-token" style="font-weight: ${token.value};">
|
||||
${token.name} (${token.value})
|
||||
</div>
|
||||
`;
|
||||
} else if (token.attributes.category === 'letter-spacing') {
|
||||
// Convert % to px
|
||||
const letterSpacingValue = token.value.replace('%', '') + 'px';
|
||||
letterSpacingTokens += `
|
||||
<div class="letter-spacing-token" style="letter-spacing: ${letterSpacingValue};">
|
||||
${token.name} (${letterSpacingValue})
|
||||
</div>
|
||||
`;
|
||||
} else if (token.attributes.category === 'space') {
|
||||
spaceTokenList.push(token); // Collect space tokens
|
||||
}
|
||||
});
|
||||
|
||||
// Sort border-radius and space tokens
|
||||
borderRadiusTokenList.sort((a, b) => extractValue(a.name) - extractValue(b.name));
|
||||
spaceTokenList.sort((a, b) => extractValue(a.name) - extractValue(b.name));
|
||||
|
||||
// Generate HTML for sorted border-radius tokens
|
||||
borderRadiusTokenList.forEach((token) => {
|
||||
borderRadiusTokens += `
|
||||
<div class="border-token" style="border-radius: ${token.value};">
|
||||
${token.name} (${token.value})
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Generate HTML for sorted space tokens
|
||||
spaceTokenList.forEach((token) => {
|
||||
spaceTokens += `
|
||||
<div class="spacing-wrapper">
|
||||
<div class="space-token" style="margin: ${token.value};">
|
||||
${token.name} (${token.value})
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
colorTokens += '</tbody></table>';
|
||||
|
||||
return template
|
||||
.replace('{{colorTokens}}', colorTokens)
|
||||
.replace('{{fontSizeTokens}}', fontSizeTokens)
|
||||
.replace('{{boxShadowTokens}}', boxShadowTokens)
|
||||
.replace('{{borderSizeTokens}}', borderSizeTokens)
|
||||
.replace('{{borderRadiusTokens}}', borderRadiusTokens)
|
||||
.replace('{{borderStyleTokens}}', borderStyleTokens)
|
||||
.replace('{{fontWeightTokens}}', fontWeightTokens)
|
||||
.replace('{{letterSpacingTokens}}', letterSpacingTokens)
|
||||
.replace('{{spaceTokens}}', spaceTokens);
|
||||
},
|
||||
});
|
||||
|
||||
// Custom transform to ensure unique token names
|
||||
StyleDictionary.registerTransform({
|
||||
name: 'name/cti/kebab-unique',
|
||||
@@ -210,6 +343,16 @@ StyleDictionary.extend({
|
||||
},
|
||||
],
|
||||
},
|
||||
html: {
|
||||
transformGroup: 'custom',
|
||||
buildPath: targetPath,
|
||||
files: [
|
||||
{
|
||||
destination: 'tokens.preview.html',
|
||||
format: 'html/tokens',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
fileHeader: {
|
||||
myFileHeader: () => {
|
||||
|
||||
63
core/scripts/tokens/preview.styles.css
Normal file
@@ -0,0 +1,63 @@
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #f4f4f4;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.font-size-token,
|
||||
.weight-token,
|
||||
.letter-spacing-token {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.border-token,
|
||||
.shadow-token {
|
||||
margin: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.border-token {
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.spacing-wrapper {
|
||||
background-color: lightblue;
|
||||
}
|
||||
|
||||
.spacing-wrapper > div {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.token-wrapper:has(.spacing-wrapper) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
hr {
|
||||
background-color: #ccc;
|
||||
margin: 20px 0;
|
||||
}
|
||||
52
core/scripts/tokens/preview.template.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
|
||||
/>
|
||||
<title>Design Tokens</title>
|
||||
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet" />
|
||||
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
|
||||
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
|
||||
<link rel="stylesheet" href="../../scripts/tokens/preview.styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<ion-app>
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Design Tokens - Preview</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content class="ion-padding-horizontal">
|
||||
<h1>Color Tokens</h1>
|
||||
<div class="token-wrapper">{{colorTokens}}</div>
|
||||
<hr />
|
||||
<h1>Font Size Tokens</h1>
|
||||
<div class="token-wrapper">{{fontSizeTokens}}</div>
|
||||
<hr />
|
||||
<h1>Font Weight Tokens</h1>
|
||||
<div class="token-wrapper">{{fontWeightTokens}}</div>
|
||||
<hr />
|
||||
<h1>Letter Spacing Tokens</h1>
|
||||
<div class="token-wrapper">{{letterSpacingTokens}}</div>
|
||||
<hr />
|
||||
<h1>Box Shadow Tokens</h1>
|
||||
<div class="token-wrapper">{{boxShadowTokens}}</div>
|
||||
<hr />
|
||||
<h1>Border Size Tokens</h1>
|
||||
<div class="token-wrapper">{{borderSizeTokens}}</div>
|
||||
<hr />
|
||||
<h1>Border Radius Tokens</h1>
|
||||
<div class="token-wrapper">{{borderRadiusTokens}}</div>
|
||||
<hr />
|
||||
<h1>Border Style Tokens</h1>
|
||||
<div class="token-wrapper">{{borderStyleTokens}}</div>
|
||||
<hr />
|
||||
<h1>Space Tokens</h1>
|
||||
<div class="token-wrapper">{{spaceTokens}}</div>
|
||||
</ion-content>
|
||||
</ion-app>
|
||||
</body>
|
||||
</html>
|
||||
8
core/src/components.d.ts
vendored
@@ -336,9 +336,9 @@ export namespace Components {
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
/**
|
||||
* Set to `"small"` for a compact size, `"medium"` for the default height and width, or to `"large"` for a larger size. Defaults to `"medium"` for the `ionic` theme, undefined for all other themes.
|
||||
* Set to `"xsmall"` for the smallest size, `"small"` for a compact size, `"medium"` for the default height and width, or to `"large"` for a larger size. Defaults to `"medium"` for the `ionic` theme, undefined for all other themes.
|
||||
*/
|
||||
"size"?: 'small' | 'medium' | 'large';
|
||||
"size"?: `xsmall` | 'small' | 'medium' | 'large';
|
||||
/**
|
||||
* The theme determines the visual appearance of the component.
|
||||
*/
|
||||
@@ -5568,9 +5568,9 @@ declare namespace LocalJSX {
|
||||
*/
|
||||
"mode"?: "ios" | "md";
|
||||
/**
|
||||
* Set to `"small"` for a compact size, `"medium"` for the default height and width, or to `"large"` for a larger size. Defaults to `"medium"` for the `ionic` theme, undefined for all other themes.
|
||||
* Set to `"xsmall"` for the smallest size, `"small"` for a compact size, `"medium"` for the default height and width, or to `"large"` for a larger size. Defaults to `"medium"` for the `ionic` theme, undefined for all other themes.
|
||||
*/
|
||||
"size"?: 'small' | 'medium' | 'large';
|
||||
"size"?: `xsmall` | 'small' | 'medium' | 'large';
|
||||
/**
|
||||
* The theme determines the visual appearance of the component.
|
||||
*/
|
||||
|
||||
@@ -28,6 +28,20 @@
|
||||
// Avatar Sizes
|
||||
// --------------------------------------------------
|
||||
|
||||
:host(.avatar-xsmall) {
|
||||
--padding-end: #{globals.$ionic-space-050};
|
||||
--padding-start: #{globals.$ionic-space-050};
|
||||
|
||||
width: globals.$ionic-scale-600;
|
||||
height: globals.$ionic-scale-600;
|
||||
|
||||
font-size: globals.$ionic-font-size-300;
|
||||
|
||||
font-weight: globals.$ionic-font-weight-medium;
|
||||
|
||||
line-height: globals.$ionic-line-height-500;
|
||||
}
|
||||
|
||||
:host(.avatar-small) {
|
||||
--padding-end: #{globals.$ionic-space-150};
|
||||
--padding-start: #{globals.$ionic-space-150};
|
||||
|
||||
@@ -20,12 +20,12 @@ export class Avatar implements ComponentInterface {
|
||||
@Element() el!: HTMLElement;
|
||||
|
||||
/**
|
||||
* Set to `"small"` for a compact size, `"medium"` for the default height and width, or to
|
||||
* `"large"` for a larger size.
|
||||
* Set to `"xsmall"` for the smallest size, `"small"` for a compact size, `"medium"`
|
||||
* for the default height and width, or to `"large"` for a larger size.
|
||||
*
|
||||
* Defaults to `"medium"` for the `ionic` theme, undefined for all other themes.
|
||||
*/
|
||||
@Prop() size?: 'small' | 'medium' | 'large';
|
||||
@Prop() size?: `xsmall` | 'small' | 'medium' | 'large';
|
||||
|
||||
get hasImage() {
|
||||
return !!this.el.querySelector('ion-img') || !!this.el.querySelector('img');
|
||||
|
||||
@@ -6,6 +6,51 @@ import { configs, test } from '@utils/test/playwright';
|
||||
*/
|
||||
configs({ directions: ['ltr'], modes: ['ionic-md'] }).forEach(({ config, screenshot, title }) => {
|
||||
test.describe(title('avatar: size'), () => {
|
||||
test.describe('xsmall', () => {
|
||||
test('should not have visual regressions when containing text', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-avatar size="xsmall">AB</ion-avatar>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const avatar = page.locator('ion-avatar');
|
||||
|
||||
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-xsmall-text`));
|
||||
});
|
||||
|
||||
test('should not have visual regressions when containing an icon', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-avatar size="xsmall">
|
||||
<ion-icon name="person-outline"></ion-icon>
|
||||
</ion-avatar>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const avatar = page.locator('ion-avatar');
|
||||
|
||||
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-xsmall-icon`));
|
||||
});
|
||||
|
||||
test('should not have visual regressions when containing an image', async ({ page }) => {
|
||||
await page.setContent(
|
||||
`
|
||||
<ion-avatar size="xsmall">
|
||||
<img src="/src/components/avatar/test/avatar.svg"/>
|
||||
</ion-avatar>
|
||||
`,
|
||||
config
|
||||
);
|
||||
|
||||
const avatar = page.locator('ion-avatar');
|
||||
|
||||
await expect(avatar).toHaveScreenshot(screenshot(`avatar-size-xsmall-image`));
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('small', () => {
|
||||
test('should not have visual regressions when containing text', async ({ page }) => {
|
||||
await page.setContent(
|
||||
|
||||
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 396 B |
|
After Width: | Height: | Size: 384 B |
|
After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 376 B |
|
After Width: | Height: | Size: 418 B |
|
After Width: | Height: | Size: 490 B |
|
After Width: | Height: | Size: 525 B |
|
After Width: | Height: | Size: 456 B |
@@ -43,6 +43,7 @@
|
||||
|
||||
<h2>Text</h2>
|
||||
<div class="container">
|
||||
<ion-avatar size="xsmall">AB</ion-avatar>
|
||||
<ion-avatar size="small">AB</ion-avatar>
|
||||
<ion-avatar size="medium">AB</ion-avatar>
|
||||
<ion-avatar size="large">AB</ion-avatar>
|
||||
@@ -50,6 +51,9 @@
|
||||
|
||||
<h2>Icons</h2>
|
||||
<div class="container">
|
||||
<ion-avatar size="xsmall">
|
||||
<ion-icon name="person-outline"></ion-icon>
|
||||
</ion-avatar>
|
||||
<ion-avatar size="small">
|
||||
<ion-icon name="person-outline"></ion-icon>
|
||||
</ion-avatar>
|
||||
@@ -63,6 +67,9 @@
|
||||
|
||||
<h2>Images</h2>
|
||||
<div class="container">
|
||||
<ion-avatar size="xsmall">
|
||||
<img src="/src/components/avatar/test/avatar.svg" />
|
||||
</ion-avatar>
|
||||
<ion-avatar size="small">
|
||||
<img src="/src/components/avatar/test/avatar.svg" />
|
||||
</ion-avatar>
|
||||
|
||||