feat: type-safe sc-icons

This commit is contained in:
2024-04-03 12:16:18 +02:00
committed by Rainer Killinger
parent 53c3d0ba0c
commit abf9999461
29 changed files with 211 additions and 172 deletions

View File

@@ -50,11 +50,11 @@ the config file.
Icon font minification is done automatically, but requires you to
follow a few simple rules:
1. Use the tagged template literal for referencing icon names in
1. Use the type-safe proxy for referencing icon names in
TypeScript files and code
```ts
SCIcon`icon_name`;
SCIcon.icon_name;
```
2. When using `ion-icon` in HTML, reference either icons that went through

View File

@@ -12,9 +12,9 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type {IconConfig} from './scripts/icon-config';
const config: IconConfig = {
/** @type {import('./scripts/icon-config').IconConfig} */
const config = {
inputPath: 'node_modules/material-symbols/material-symbols-rounded.woff2',
outputPath: 'src/assets/icons.min.woff2',
htmlGlob: 'src/**/*.html',
@@ -38,10 +38,6 @@ const config: IconConfig = {
'work',
],
},
codePoints: {
ios_share: 'e6b8',
fact_check: 'f0c5',
},
};
export default config;

View File

@@ -21,7 +21,7 @@
"build:prod": "ng build --configuration=production",
"build:stats": "ng build --configuration=production --stats-json",
"changelog": "conventional-changelog -p angular -i src/assets/about/CHANGELOG.md -s -r 0",
"check-icons": "ts-node scripts/check-icon-correctness.ts",
"check-icons": "node scripts/check-icon-correctness.mjs",
"chromium:no-cors": "chromium --disable-web-security --user-data-dir=\".browser-data/chromium\"",
"chromium:virtual-host": "chromium --host-resolver-rules=\"MAP mobile.app.uni-frankfurt.de:* localhost:8100\" --ignore-certificate-errors",
"cypress:open": "cypress open",
@@ -35,10 +35,16 @@
"e2e": "ng e2e",
"format": "prettier . -c",
"format:fix": "prettier --write .",
"licenses": "license-checker --json > src/assets/about/licenses.json && ts-node ./scripts/accumulate-licenses.ts && git add src/assets/about/licenses.json",
"licenses": "license-checker --json > src/assets/about/licenses.json && node ./scripts/accumulate-licenses.mjs && git add src/assets/about/licenses.json",
"lint": "ng lint && stylelint \"**/*.scss\"",
"lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/ && stylelint --fix \"**/*.scss\"",
<<<<<<< HEAD
"minify-icons": "ts-node-esm scripts/minify-icon-font.ts",
||||||| parent of 7b431b8f (feat: type-safe sc-icons)
"minify-icons": "ts-node scripts/minify-icon-font.ts",
=======
"minify-icons": "node scripts/minify-icon-font.mjs",
>>>>>>> 7b431b8f (feat: type-safe sc-icons)
"postinstall": "jetify && echo \"skipping jetify in production mode\"",
"preview": "http-server www --p 8101 -o",
"push": "git push && git push origin \"v$npm_package_version\"",

View File

@@ -12,33 +12,34 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import fs from 'fs';
import {omit} from '../src/app/_helpers/collections/omit';
import {pickBy} from '../src/app/_helpers/collections/pick';
import {readFileSync, writeFileSync} from 'fs';
import {omit, pickBy} from '@openstapps/collection-utils';
/**
* accumulate and transform licenses based on two license files
* @param {string} path
* @param {string} additionalLicensesPath
*/
function accumulateFile(path: string, additionalLicensesPath: string) {
const packageJson = JSON.parse(fs.readFileSync('./package.json').toString());
function accumulateFile(path, additionalLicensesPath) {
const packageJson = JSON.parse(readFileSync('./package.json').toString());
const dependencies = packageJson.dependencies;
console.log(`Accumulating licenses from ${path}`);
fs.writeFileSync(
writeFileSync(
path,
JSON.stringify(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Object.entries<any>({
...pickBy(JSON.parse(fs.readFileSync(path).toString()), (_, key: string) => {
const parts = key.split('@');
Object.entries({
...pickBy(JSON.parse(readFileSync(path).toString()), (_, key) => {
const parts = /** @type {string} */ (key).split('@');
return dependencies[parts.slice(0, -1).join('@')] === parts[parts.length - 1];
}),
...JSON.parse(fs.readFileSync(additionalLicensesPath).toString()),
...JSON.parse(readFileSync(additionalLicensesPath).toString()),
})
.map(([key, value]) => ({
licenseText: value.licenseFile && fs.readFileSync(value.licenseFile, 'utf8'),
licenseText: value.licenseFile && readFileSync(value.licenseFile, 'utf8'),
name: key,
...omit(value, 'licenseFile', 'path'),
}))

View File

@@ -12,18 +12,20 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import fontkit, {Font} from 'fontkit';
import config from '../icons.config';
import {openSync} from 'fontkit';
import config from '../icons.config.mjs';
import {existsSync} from 'fs';
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons';
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons.mjs';
import {fetchCodePointMap} from './get-code-points.mjs';
const commandName = '"npm run minify-icons"';
const originalFont = fontkit.openSync(config.inputPath);
if (!existsSync(config.outputPath)) {
console.error(`Minified font not found. Run ${commandName} first.`);
process.exit(-1);
}
const modifiedFont = fontkit.openSync(config.outputPath);
/** @type {import('fontkit').Font} */
const modifiedFont = openSync(config.outputPath);
let success = true;
@@ -48,25 +50,16 @@ async function checkAll() {
}
/**
*
* @param {Record<string, string[]>} icons
*/
function check(icons: Record<string, string[]>) {
for (const [purpose, iconSet] of Object.entries(icons)) {
for (const icon of iconSet) {
if (!hasIcon(originalFont, icon)) {
success = false;
console.error(`${purpose}: ${icon} does not exist. Typo?`);
} else if (!hasIcon(modifiedFont, icon)) {
success = false;
console.error(`${purpose}: ${icon} not found in minified font. Run ${commandName} to regenerate it.`);
}
async function check(icons) {
const codePoints = await fetchCodePointMap();
for (const icon of Object.values(icons).flat()) {
const codePoint = codePoints.get(icon);
if (!codePoint) throw new Error(`"${icon}" is not a valid icon`);
if (!modifiedFont.getGlyph(Number.parseInt(codePoint, 16))) {
throw new Error(`"${icon}" (code point ${codePoint}) is missing`);
}
}
}
/**
*
*/
function hasIcon(font: Font, icon: string) {
return font.layout(icon).glyphs.some(it => it.isLigature);
}

View File

@@ -14,34 +14,39 @@
*/
import {glob} from 'glob';
import {readFileSync} from 'fs';
import {matchPropertyContent, matchTagProperties} from '../src/app/util/ion-icon/icon-match';
import {
matchPropertyAccess,
matchPropertyContent,
matchTagProperties,
} from '../src/app/util/ion-icon/icon-match.mjs';
/**
*
* @returns {Promise<Record<string, string[]>>}
*/
export async function getUsedIconsHtml(pattern = 'src/**/*.html'): Promise<Record<string, string[]>> {
export async function getUsedIconsHtml(pattern = 'src/**/*.html') {
return Object.fromEntries(
(await glob(pattern))
.map(file => [
file,
(readFileSync(file, 'utf8')
readFileSync(file, 'utf8')
.match(matchTagProperties('ion-icon'))
?.flatMap(match => {
return match.match(matchPropertyContent(['name', 'md', 'ios']));
})
.filter(it => !!it) as string[]) || [],
.filter(it => !!it) || [],
])
.filter(([, values]) => values.length > 0),
.filter(([, values]) => values && values.length > 0),
);
}
/**
*
* @returns {Promise<Record<string, string[]>>}
*/
export async function getUsedIconsTS(pattern = 'src/**/*.ts'): Promise<Record<string, string[]>> {
export async function getUsedIconsTS(pattern = 'src/**/*.ts') {
const regex = matchPropertyAccess('SCIcon');
return Object.fromEntries(
(await glob(pattern))
.map(file => [file, readFileSync(file, 'utf8').match(/(?<=Icon`)[\w-]+(?=`)/g) || []])
.filter(([, values]) => values.length > 0),
.map(file => [file, readFileSync(file, 'utf8').match(regex) || []])
.filter(([, values]) => values && values.length > 0),
);
}

View File

@@ -0,0 +1,23 @@
const url =
'https://raw.githubusercontent.com/google/material-design-icons/master/' +
'variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.codepoints';
export async function fetchCodePointMap() {
const icons = await fetch(url)
.then(it => it.text())
.then(it => new Map(it.split('\n').map(it => /** @type {[string, string]} */ (it.split(' ')))));
if (icons.size < 100) throw new Error(`Code point map is very small, is the URL incorrect? ${url}`);
return icons;
}
/**
* @param {string[]} icons
*/
export async function getCodePoints(icons) {
const codePoints = await fetchCodePointMap();
return icons.map(icon => {
const code = codePoints.get(icon);
if (!code) throw new Error(`Code point for icon ${icon} not found`);
return code;
});
}

View File

@@ -19,5 +19,4 @@ export interface IconConfig {
inputPath: string;
outputPath: string;
additionalIcons?: {[purpose: string]: string[]};
codePoints?: {[name: string]: string};
}

View File

@@ -14,16 +14,17 @@
*/
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import fontkit from 'fontkit';
import {exec} from 'child_process';
import config from '../icons.config';
import config from '../icons.config.mjs';
import {statSync} from 'fs';
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons';
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons.mjs';
import {getCodePoints} from './get-code-points.mjs';
/**
*
* @param {string[] | string} command
* @returns {Promise<string>}
*/
async function run(command: string[] | string): Promise<string> {
async function run(command) {
const fullCommand = Array.isArray(command) ? command.join(' ') : command;
console.log(`>> ${fullCommand}`);
@@ -44,7 +45,8 @@ async function run(command: string[] | string): Promise<string> {
*
*/
async function minifyIconFont() {
const icons = new Set<string>();
/** @type {Set<string>} */
const icons = new Set();
for (const iconSet of [
...Object.values(config.additionalIcons || []),
@@ -56,35 +58,7 @@ async function minifyIconFont() {
}
}
console.log('Icons used:', [...icons.values()].sort());
const font = fontkit.openSync(config.inputPath);
const glyphs: string[] = ['5f-7a', '30-39'];
for (const icon of icons) {
const iconGlyphs = font.layout(icon).glyphs;
if (iconGlyphs.length === 0) {
console.error(`${icon} not found in font. Typo?`);
process.exit(-1);
}
const codePoints = iconGlyphs
.flatMap(it => font.stringsForGlyph(it.id))
.flatMap(it => [...it])
.map(it => it.codePointAt(0)!.toString(16));
if (codePoints.length === 0) {
if (config.codePoints?.[icon]) {
glyphs.push(config.codePoints[icon]);
} else {
console.log();
console.error(`${icon} code point could not be determined. Add it to config.codePoints.`);
process.exit(-1);
}
}
glyphs.push(...codePoints);
}
glyphs.sort();
const glyphs = ['5f-7a', '30-39', ...(await getCodePoints([...icons]))].sort();
console.log(
await run([
@@ -114,8 +88,10 @@ minifyIconFont();
/**
* Bytes to respective units
* @param {number} value
* @returns {string}
*/
function toByteUnit(value: number): string {
function toByteUnit(value) {
if (value < 1024) {
return `${value}B`;
} else if (value < 1024 * 1024) {

View File

@@ -49,7 +49,7 @@
@if (content.type === 'router link') {
<ion-item [routerLink]="content.link">
@if (content.icon) {
<ion-icon [name]="content.icon" slot="start"></ion-icon>
<ion-icon [name]="$any(content.icon)" slot="start"></ion-icon>
}
<ion-label>{{ 'title' | translateSimple: content }}</ion-label>
</ion-item>

View File

@@ -106,7 +106,7 @@ describe('ConfigProvider', () => {
storageProviderSpy.has.and.returnValue(Promise.resolve(true));
const wrongConfig = structuredClone(sampleIndexResponse);
wrongConfig.backend.SCVersion = '0.1.0';
storageProviderSpy.get.and.returnValue(wrongConfig);
storageProviderSpy.get.and.returnValue(Promise.resolve(wrongConfig));
spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));
await configProvider.init();

View File

@@ -26,6 +26,7 @@ import {AddEventStates, AddEventStatesMap} from './add-event-action-chip.config'
import {EditEventSelectionComponent} from '../edit-event-selection.component';
import {AddEventReviewModalComponent} from '../../../calendar/add-event-review-modal.component';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {MaterialSymbol} from 'material-symbols';
/**
* Shows a horizontal list of action chips
@@ -55,7 +56,7 @@ export class AddEventActionChipComponent {
/**
* Icon
*/
icon: string;
icon: MaterialSymbol;
/**
* Current state of icon fill

View File

@@ -24,28 +24,28 @@ export enum AddEventStates {
export const AddEventStatesMap = {
[AddEventStates.ADDED_ALL]: {
icon: SCIcon`event_available`,
icon: SCIcon.event_available,
fill: true,
label: 'data.chips.add_events.ADDED_ALL',
disabled: false,
color: 'success',
},
[AddEventStates.ADDED_SOME]: {
icon: SCIcon`event`,
icon: SCIcon.event,
fill: true,
label: 'data.chips.add_events.ADDED_SOME',
disabled: false,
color: 'success',
},
[AddEventStates.REMOVED_ALL]: {
icon: SCIcon`calendar_today`,
icon: SCIcon.calendar_today,
fill: false,
label: 'data.chips.add_events.REMOVED_ALL',
disabled: false,
color: 'primary',
},
[AddEventStates.UNAVAILABLE]: {
icon: SCIcon`event_busy`,
icon: SCIcon.event_busy,
fill: false,
label: 'data.chips.add_events.UNAVAILABLE',
disabled: true,

View File

@@ -14,37 +14,38 @@
*/
import {SCThingType} from '@openstapps/core';
import {SCIcon} from '../../util/ion-icon/icon';
import {MaterialSymbol} from 'material-symbols';
export const DataIcons: Record<SCThingType, string> = {
'academic event': SCIcon`school`,
'assessment': SCIcon`fact_check`,
'article': SCIcon`article`,
'book': SCIcon`book`,
'building': SCIcon`location_city`,
'certification': SCIcon`contract`,
'catalog': SCIcon`inventory_2`,
'contact point': SCIcon`contact_page`,
'course of study': SCIcon`school`,
'date series': SCIcon`event`,
'dish': SCIcon`lunch_dining`,
'favorite': SCIcon`favorite`,
'floor': SCIcon`foundation`,
'id card': SCIcon`badge`,
'message': SCIcon`newspaper`,
'organization': SCIcon`business_center`,
'periodical': SCIcon`feed`,
'person': SCIcon`person`,
'point of interest': SCIcon`pin_drop`,
'publication event': SCIcon`campaign`,
'room': SCIcon`meeting_room`,
'semester': SCIcon`date_range`,
'setting': SCIcon`settings`,
'sport course': SCIcon`sports_soccer`,
'study module': SCIcon`view_module`,
'ticket': SCIcon`confirmation_number`,
'todo': SCIcon`task`,
'tour': SCIcon`tour`,
'video': SCIcon`movie`,
'diff': SCIcon`difference`,
'job posting': SCIcon`work`,
};
export const DataIcons = {
'academic event': SCIcon.school,
'assessment': SCIcon.fact_check,
'article': SCIcon.article,
'book': SCIcon.book,
'building': SCIcon.location_city,
'certification': SCIcon.contract,
'catalog': SCIcon.inventory_2,
'contact point': SCIcon.contact_page,
'course of study': SCIcon.school,
'date series': SCIcon.event,
'dish': SCIcon.lunch_dining,
'favorite': SCIcon.favorite,
'floor': SCIcon.foundation,
'id card': SCIcon.badge,
'message': SCIcon.newspaper,
'organization': SCIcon.business_center,
'periodical': SCIcon.feed,
'person': SCIcon.person,
'point of interest': SCIcon.pin_drop,
'publication event': SCIcon.campaign,
'room': SCIcon.meeting_room,
'semester': SCIcon.date_range,
'setting': SCIcon.settings,
'sport course': SCIcon.sports_soccer,
'study module': SCIcon.view_module,
'ticket': SCIcon.confirmation_number,
'todo': SCIcon.task,
'tour': SCIcon.tour,
'video': SCIcon.movie,
'diff': SCIcon.difference,
'job posting': SCIcon.work,
} satisfies Record<SCThingType, MaterialSymbol>;

View File

@@ -31,7 +31,7 @@ export class DataIconPipe implements PipeTransform {
/**
* Provide the icon name from the data type
*/
transform(type: SCThingType): string {
transform(type: SCThingType) {
return this.typeIconMap[type];
}
}

View File

@@ -18,8 +18,8 @@ import {SCThings} from '@openstapps/core';
import {SCIcon} from '../../../util/ion-icon/icon';
const AccordionButtonState = {
collapsed: SCIcon`expand_more`,
expanded: SCIcon`expand_less`,
collapsed: SCIcon.expand_more,
expanded: SCIcon.expand_less,
};
@Component({
@@ -35,7 +35,8 @@ export class TitleCardComponent implements OnInit, OnChanges {
@ViewChild('accordionTextArea') accordionTextArea: ElementRef;
buttonState = AccordionButtonState.collapsed;
buttonState: (typeof AccordionButtonState)[keyof typeof AccordionButtonState] =
AccordionButtonState.collapsed;
buttonShown = true;

View File

@@ -35,13 +35,13 @@
lines="none"
class="menu-category"
>
<ion-icon slot="end" [name]="category.icon"></ion-icon>
<ion-icon slot="end" [name]="$any(category.icon)"></ion-icon>
<ion-label> {{ category.translations[language]?.title | titlecase }} </ion-label>
</ion-item>
}
@for (item of category.items; track item) {
<ion-item [rootLink]="item.route" [redirectedFrom]="item.route">
<ion-icon slot="end" [name]="item.icon"></ion-icon>
<ion-icon slot="end" [name]="$any(item.icon)"></ion-icon>
<ion-label> {{ item.translations[language]?.title | titlecase }} </ion-label>
</ion-item>
}

View File

@@ -41,7 +41,7 @@
</ion-menu-toggle>
@for (category of menu; track category; let isFirst = $first) {
<ion-tab-button [rootLink]="category.route" [redirectedFrom]="category.route" [tab]="category.title">
<ion-icon [name]="category.icon"></ion-icon>
<ion-icon [name]="$any(category.icon)"></ion-icon>
<ion-label>{{ category.translations[language]?.title | titlecase }}</ion-label>
</ion-tab-button>
}

View File

@@ -35,7 +35,7 @@
>
<div>
@if (link.icon) {
<ion-icon [name]="link.icon" [size]="36" color="dark"></ion-icon>
<ion-icon [name]="$any(link.icon)" [size]="36" color="dark"></ion-icon>
}
<ion-label>{{ 'name' | translateSimple: link }}</ion-label>
</div>

View File

@@ -0,0 +1,5 @@
export function matchTagProperties(tag: string): RegExp;
export function matchPropertyContent(properties: string[]): RegExp;
export function matchPropertyAccess(objectName: string): RegExp;

View File

@@ -14,17 +14,24 @@
*/
/**
*
* @param {string} tag
*/
export function matchTagProperties(tag: string) {
export function matchTagProperties(tag) {
return new RegExp(`(?<=<${tag})[\\s\\S]*?(?=>\\s*<\\/${tag}\\s*>)`, 'g');
}
/**
*
* @param {string[]} properties
*/
export function matchPropertyContent(properties: string[]) {
export function matchPropertyContent(properties) {
const names = properties.join('|');
return new RegExp(`((?<=(${names})=")[\\w-]+(?="))|((?<=\\[(${names})]="')[\\w-]+(?='"))`, 'g');
}
/**
* @param {string} objectName
*/
export function matchPropertyAccess(objectName) {
return new RegExp(`(?<=${objectName}\\s*\\.\\s*)\\w+`, 'g');
}

View File

@@ -12,9 +12,7 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* eslint-disable unicorn/no-null */
import {matchPropertyContent, matchTagProperties} from './icon-match';
import {matchPropertyAccess, matchPropertyContent, matchTagProperties} from './icon-match.mjs';
describe('matchTagProperties', function () {
const regex = matchTagProperties('test');
@@ -59,3 +57,30 @@ describe('matchPropertyContent', function () {
expect(`no="content" [no]="'content'"`.match(regex)).toEqual(null);
});
});
describe('matchPropertyAccess', function () {
const property = '0_20a_boninAo0_';
const object = 'test';
const regex = matchPropertyAccess(object);
it('should match property access', function () {
expect(`${object}.${property}`.match(regex)).toEqual([property]);
});
it('should respect whitespace', function () {
expect(`${object}. ${property}`.match(regex)).toEqual([property]);
expect(`${object} .${property}`.match(regex)).toEqual([property]);
expect(`${object} . ${property}`.match(regex)).toEqual([property]);
expect(`${object} \n . \n ${property}`.match(regex)).toEqual([property]);
});
it('should not include invalid trailing stuff', function () {
expect(`${object}.${property}!`.match(regex)).toEqual([property]);
expect(`${object}.${property}.`.match(regex)).toEqual([property]);
expect(`${object}.${property}-`.match(regex)).toEqual([property]);
expect(`${object}.${property}]`.match(regex)).toEqual([property]);
expect(`${object}.${property}}`.match(regex)).toEqual([property]);
expect(`${object}.${property};`.match(regex)).toEqual([property]);
expect(`${object}.${property}:`.match(regex)).toEqual([property]);
});
});

View File

@@ -13,9 +13,8 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* A noop function to aid parsing icon names
*/
export function SCIcon(strings: TemplateStringsArray, ..._keys: string[]): string {
return strings.join('');
}
import {MaterialSymbol} from 'material-symbols';
export const SCIcon = new Proxy({} as {[key in MaterialSymbol]: key}, {
get: (_target, prop: string) => prop as MaterialSymbol,
});

View File

@@ -48,8 +48,8 @@ export class IonBackButtonDirective extends IconReplacer implements OnInit {
replace() {
this.replaceIcon(this.host.querySelector('.button-inner'), {
md: SCIcon`arrow_back`,
ios: SCIcon`arrow_back_ios`,
md: SCIcon.arrow_back,
ios: SCIcon.arrow_back_ios,
size: 24,
});
}

View File

@@ -28,7 +28,7 @@ export class IonBreadcrumbDirective extends IconReplacer {
this.replaceIcon(
this.host.querySelector('span[part="separator"]'),
{
name: SCIcon`arrow_forward_ios`,
name: SCIcon.arrow_forward_ios,
size: 16,
style: `color: var(--ion-color-tint);`,
},
@@ -37,7 +37,7 @@ export class IonBreadcrumbDirective extends IconReplacer {
this.replaceIcon(
this.host.querySelector('button[part="collapsed-indicator"]'),
{
name: SCIcon`more_horiz`,
name: SCIcon.more_horiz,
size: 24,
},
'-collapsed',

View File

@@ -28,6 +28,7 @@ import {
} from '@angular/core';
import {IconComponent} from './icon.component';
import {IonIcon} from '@ionic/angular';
import {MaterialSymbol} from 'material-symbols';
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
@@ -40,7 +41,7 @@ const noopProperty = {
selector: 'ion-icon',
})
export class IonIconDirective implements OnInit, OnDestroy, OnChanges {
@Input() name: string;
@Input() name: MaterialSymbol;
@Input() md: string;

View File

@@ -26,7 +26,7 @@ export class IonReorderDirective extends IconReplacer {
replace() {
this.replaceIcon(this.host, {
name: SCIcon`reorder`,
name: SCIcon.reorder,
size: 24,
});
}

View File

@@ -26,11 +26,11 @@ export class IonSearchbarDirective extends IconReplacer {
replace() {
this.replaceIcon(this.host.querySelector('.searchbar-input-container'), {
name: SCIcon`search`,
name: SCIcon.search,
size: 24,
});
this.replaceIcon(this.host.querySelector('.searchbar-clear-button'), {
name: SCIcon`close`,
name: SCIcon.close,
size: 24,
});
}

View File

@@ -62,7 +62,7 @@ export const profilePageSections: SCSection[] = [
links: [
{
name: 'Favorites',
icon: SCIcon`grade`,
icon: SCIcon.grade,
link: ['/favorites'],
translations: {
de: {
@@ -73,7 +73,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Schedule',
icon: SCIcon`calendar_today`,
icon: SCIcon.calendar_today,
link: ['/schedule'],
translations: {
de: {
@@ -84,7 +84,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Course Catalog',
icon: SCIcon`inventory_2`,
icon: SCIcon.inventory_2,
link: ['/catalog'],
translations: {
de: {
@@ -95,7 +95,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Settings',
icon: SCIcon`settings`,
icon: SCIcon.settings,
link: ['/settings'],
translations: {
de: {
@@ -106,7 +106,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Feedback',
icon: SCIcon`rate_review`,
icon: SCIcon.rate_review,
link: ['/feedback'],
translations: {
de: {
@@ -117,7 +117,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'About',
icon: SCIcon`info`,
icon: SCIcon.info,
link: ['/about'],
translations: {
de: {
@@ -140,7 +140,7 @@ export const profilePageSections: SCSection[] = [
links: [
{
name: 'Assessments',
icon: SCIcon`fact_check`,
icon: SCIcon.fact_check,
link: ['/assessments'],
needsAuth: true,
translations: {
@@ -164,7 +164,7 @@ export const profilePageSections: SCSection[] = [
links: [
{
name: 'Library Catalog',
icon: SCIcon`local_library`,
icon: SCIcon.local_library,
link: ['/hebis-search'],
translations: {
de: {
@@ -175,7 +175,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Library Account',
icon: SCIcon`badge`,
icon: SCIcon.badge,
needsAuth: true,
link: ['/library-account/profile'],
translations: {
@@ -187,7 +187,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Orders & Reservations',
icon: SCIcon`collections_bookmark`,
icon: SCIcon.collections_bookmark,
needsAuth: true,
link: ['/library-account/holds'],
translations: {
@@ -199,7 +199,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Checked out items',
icon: SCIcon`library_books`,
icon: SCIcon.library_books,
needsAuth: true,
link: ['/library-account/checked-out'],
translations: {
@@ -211,7 +211,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Fines',
icon: SCIcon`request_page`,
icon: SCIcon.request_page,
needsAuth: true,
link: ['/library-account/fines'],
translations: {