init webseite ts and claude

This commit is contained in:
Philippe Torrel
2026-07-31 13:02:11 +02:00
parent e375bf23bc
commit 56295802c0
617 changed files with 94590 additions and 5826 deletions

View File

@@ -509,7 +509,7 @@ Projekte + weiteres Modul einbinden
**Übungen:**
Übungen 10 - 18
Übungen 10 - 19
---
@@ -519,11 +519,10 @@ Projekte + weiteres Modul einbinden
- TS Funktionen und Objekte
- Abschluss TypeScript Grundlagen
- Abschlussquiz
**Übungen:**
Übungen 19 - 31
Übungen 20 - 31
Projektarbeit
---
@@ -532,6 +531,7 @@ Projektarbeit
**Inhalt:**
- Abschlussquiz
- Projekt-Webseite in TS
- Optimierung mit KI
- 1:1 Meeting

View File

@@ -0,0 +1,10 @@
const greet = () => {
return 'Hello!';
};
greet.description = 'Function that says hello';
greet.anotherProp = 'Wert';
console.log(greet());
console.log(greet.description);
console.log(greet.anotherProp);

View File

@@ -0,0 +1,29 @@
// Type - describable Function
type GreetFn = {
anotherProp: string;
(): void;
};
// Type - Object
type Greet = {
prop: string;
greed: () => void;
};
const greetObj: Greet = {
prop: 'bin ein objekt',
greed: () => {
console.log('hello');
},
};
const greet: GreetFn = () => {
return 'Hello!';
};
// greet.description = 'Function that says hello';
greet.anotherProp = 'Wert';
console.log(greet());
// console.log(greet.description);
console.log(greet.anotherProp);

View File

@@ -0,0 +1,33 @@
(() => {
var __getOwnPropNames = Object.getOwnPropertyNames;
var __commonJS = (cb, mod) => function __require() {
try {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
} catch (e) {
throw mod = 0, e;
}
};
// src/main.ts
var require_main = __commonJS({
"src/main.ts"() {
(() => {
const double = (num) => num * 2;
const halve = (num) => num / 2;
const describeAndApply = (dnObj, num) => {
console.log(dnObj.description);
return dnObj(num);
};
const describableDouble = Object.assign(double, {
description: "This operation doubles the number."
});
const describableHalve = Object.assign(halve, {
description: "This operation halves the number."
});
console.log(describeAndApply(describableDouble, 10));
console.log(describeAndApply(describableHalve, 10));
})();
}
});
require_main();
})();

View File

@@ -1,38 +1,43 @@
// Step 1: Define the NumberOperation call signature
(() => {
// Übung 20: Call Signatures für Zahlenoperationen
// Step 2: Implement double and halve functions
// In dieser Übung erstellst und verwendest du ein paar einfache Funktionen, die sich an bestimmte Call Signatures für die Arbeit mit Zahlen halten.
// Step 3: Define the DescribableNumberOperation type
// Anweisungen:
// Step 4: Implement describeAndApply function
// 1 Definiere eine Funktion vom Typ NumberOperation, die ein einzelnes number annimmt und ein number zurückgibt.
// Test all functions
const describableDouble: DescribableNumberOperation = Object.assign(double, {
description: 'This operation doubles the number.',
});
type NumberOperation = (num: number) => number;
const describableHalve: DescribableNumberOperation = Object.assign(halve, {
description: 'This operation halves the number.',
});
// 2 Implementiere zwei Funktionen, double und halve, die der Call Signature NumberOperation folgen:
console.log(describeAndApply(describableDouble, 10)); // => 20
console.log(describeAndApply(describableHalve, 10)); // => 5
// double: Multipliziert die eingegebene Zahl mit 2.
const double: NumberOperation = (num) => num * 2; // keine geschweiften Klammern
// Übung 20: Call Signatures für Zahlenoperationen
// halve: Teilt die eingegebene Zahl durch 2.
const halve: NumberOperation = (num) => num / 2;
// In dieser Übung erstellst und verwendest du ein paar einfache Funktionen, die sich an bestimmte Call Signatures für die Arbeit mit Zahlen halten.
// 3 Definiere einen Objekttyp DescribableNumberOperation mit einer description-Property vom Typ string und einer Cal Signature, die NumberOperation entspricht.
type DescribableNumberOperation = {
description: string;
(num: number): number;
};
// Anweisungen:
// 4 Implementiere eine Funktion describeAndApply, die ein DescribableNumberOperation Objekt annimmt, dessen description protokolliert und die Operation auf eine bestimmte Zahl anwendet.
const describeAndApply = (dnObj: DescribableNumberOperation, num: number) => {
console.log(dnObj.description);
return dnObj(num);
};
// 1 Definiere eine Funktion vom Typ NumberOperation, die ein einzelnes number annimmt und ein number zurückgibt.
// 5 Teste alle Funktionen mit ein paar Zahlen.
const describableDouble: DescribableNumberOperation = Object.assign(double, {
description: 'This operation doubles the number.',
});
// 2 Implementiere zwei Funktionen, double und halve, die der Call Signature NumberOperation folgen:
const describableHalve: DescribableNumberOperation = Object.assign(halve, {
description: 'This operation halves the number.',
});
// double: Multipliziert die eingegebene Zahl mit 2.
// halve: Teilt die eingegebene Zahl durch 2.
// 3 Definiere einen Objekttyp DescribableNumberOperation mit einer description-Property vom Typ string und einer Cal Signature, die NumberOperation entspricht.
// 4 Implementiere eine Funktion describeAndApply, die ein DescribableNumberOperation Objekt annimmt, dessen description protokolliert und die Operation auf eine bestimmte Zahl anwendet.
// 5 Teste alle Funktionen mit ein paar Zahlen.
console.log(describeAndApply(describableDouble, 10)); // => 20
console.log(describeAndApply(describableHalve, 10)); // => 5
})();

View File

@@ -1,15 +1,33 @@
// Übung 21: Eine allgemeine Identitätsfunktion erstellen
(() => {
// Übung 21: Eine allgemeine Identitätsfunktion erstellen
// In dieser Übung erstellst du eine einfache generische Funktion namens identity, die den ihr übergebenen Wert zurückgibt. So lernst du, wie generische Funktionen mit verschiedenen Typen arbeiten können, ohne die Typsicherheit zu verlieren.
// In dieser Übung erstellst du eine einfache generische Funktion namens identity, die den ihr übergebenen Wert zurückgibt. So lernst du, wie generische Funktionen mit verschiedenen Typen arbeiten können, ohne die Typsicherheit zu verlieren.
// Anweisungen:
// Anweisungen:
// 1 Implementiere eine generische identity Funktion, die einen Parameter vom Typ T annimmt und denselben Wert zurückgibt.
// 1 Implementiere eine generische identity Funktion, die einen Parameter vom Typ T annimmt und denselben Wert zurückgibt.
// 2 Rufe identity mit einer number auf und speichere das Ergebnis.
const identity = <T>(x: T): T => {
return x;
};
// 3 Rufe identity mit einer string auf und speichere das Ergebnis.
// 2 Rufe identity mit einer number auf und speichere das Ergebnis.
// 4 Rufe identity mit einem Array von Objekten auf und speichere das Ergebnis.
const resultNum = identity<number>(2);
// 5 Stelle sicher, dass TypeScript die Rückgabetypen ohne explicit Type Annotations korrekt herleitet.
// 3 Rufe identity mit einer string auf und speichere das Ergebnis.
const resultString = identity<string>('John');
// 4 Rufe identity mit einem Array von Objekten auf und speichere das Ergebnis.
const resultArr = identity<object[]>([
{ firstName: 'John' },
{ lastName: 'Wick' },
{ firstName: 'Jane', lastName: 'Doe' },
]);
// 5 Stelle sicher, dass TypeScript die Rückgabetypen ohne explicit Type Annotations korrekt herleitet.
console.log(`Num: ${resultNum}, String: ${resultString}, Arr: ${JSON.stringify(resultArr)}`);
})();

View File

@@ -1,19 +1,26 @@
createUserProfile('JohnDoe', 25, 'john@example.com');
// => "User JohnDoe is 25 years old and can be contacted at john@example.com."
(() => {
// Übung 22: Optionale Parameter implementieren
createUserProfile('JaneDoe');
// => "User JaneDoe is 18 years old and can be contacted at N/A."
// Erstelle die Funktion createUserProfile, die drei Parameter benötigt: einen obligatorischen username und zwei optionale Parameter, age und email. Die Funktion sollte einen String mit einer Profilzusammenfassung zurückgeben. Wenn keine optionalen Parameter angegeben werden, sollten die Standardwerte verwendet werden, um das Profil zu vervollständigen.
// Übung 22: Optionale Parameter implementieren
// Anweisungen:
// Erstelle die Funktion createUserProfile, die drei Parameter benötigt: einen obligatorischen username und zwei optionale Parameter, age und email. Die Funktion sollte einen String mit einer Profilzusammenfassung zurückgeben. Wenn keine optionalen Parameter angegeben werden, sollten die Standardwerte verwendet werden, um das Profil zu vervollständigen.
// Definiere eine Funktion createUserProfile, die akzeptiert:
// Anweisungen:
// username (erforderlich, String)
// age (optional, Zahl, Standardwert ist 18)
// email (optional, String, Standardwert: "N/A")
// Definiere eine Funktion createUserProfile, die akzeptiert:
// Keine Fragezeichen nötig
const createUserProfile = (username: string, age: number = 18, email: string = 'N/A') => {
return `User ${username} is ${age} years old and can be contacted at ${email}.`;
};
// username (erforderlich, String)
// age (optional, Zahl, Standardwert ist 18)
// email (optional, String, Standardwert: "N/A")
// Die Funktion sollte einen String zurückgeben, der das Profil des Nutzers zusammenfasst.
// Die Funktion sollte einen String zurückgeben, der das Profil des Nutzers zusammenfasst.
console.log(createUserProfile('JohnDoe', 25, 'john@example.com'));
// => "User JohnDoe is 25 years old and can be contacted at john@example.com."
console.log(createUserProfile('JaneDoe'));
// => "User JaneDoe is 18 years old and can be contacted at N/A."
})();

View File

@@ -1,23 +1,42 @@
// Example usage of sumAll
// Define total
console.log(total); // => 15
(() => {
// Übung 23: Restparameter und Restargumente
// Example usage of calculateHypotenuse with spread syntax
// Define hypotenuse
console.log(hypotenuse); // => 5
// Schreibe zwei Funktionen: eine, die Restparameter verwendet, um eine unterschiedliche Anzahl von Zahlen zu akzeptieren und ihre Summe zu berechnen, und eine andere, die die Spread-Syntax verwendet, um ein Array von Zahlen in einzelne Argumente für eine mathematische Funktion zu verteilen.
// Attempting to call calculateHypotenuse with incorrect number of arguments
const invalidSides = [3, 4, 5];
// Define invalidHypotenuse to call calculateHypotenuse with invalidSides => TypeScript Error: Expected 2 arguments, but got 3.
// Anweisungen:
// Übung 23: Restparameter und Restargumente
// 1 Definiere eine Funktion sumAll, die einen Restparameter, bestehend aus einer variablen Anzahl von numerischen Argumenten, annimmt und die Summe aller übergebenen Zahlen zurückgibt. Verwende die Methode reduce, um die Summe des Arrays zu berechnen.
// Schreibe zwei Funktionen: eine, die Restparameter verwendet, um eine unterschiedliche Anzahl von Zahlen zu akzeptieren und ihre Summe zu berechnen, und eine andere, die die Spread-Syntax verwendet, um ein Array von Zahlen in einzelne Argumente für eine mathematische Funktion zu verteilen.
const sumAll = (...nums: number[]) => {
return nums.reduce((sum, num) => sum + num, 0);
};
// Anweisungen:
// 2 Als Nächstes definierst du eine Funktion namens calculateHypotenuse, die zwei Zahlen als Argumente akzeptiert und die Hypotenuse mithilfe der Formel √(a² + b²) berechnet. Du solltest diese Funktion aufrufen, indem du ein Array mit zwei Zahlen mit Hilfe der Spread-Syntax ausbreitest. (Tipp: Verwende Math.sqrt())
const calculateHypotenuse = (...[n1, n2]: [number, number]): number => {
return Math.sqrt(n1 ** 2 + n2 ** 2);
};
// 1 Definiere eine Funktion sumAll, die einen Restparameter, bestehend aus einer variablen Anzahl von numerischen Argumenten, annimmt und die Summe aller übergebenen Zahlen zurückgibt. Verwende die Methode reduce, um die Summe des Arrays zu berechnen.
// 3 Teste schließlich die Funktion sumAll, indem du sie mit mehreren Zahlen aufrufst und das Ergebnis protokollierst. Teste die Funktion calculateHypotenuse, indem du ein Array mit zwei Zahlen erstellst, dieses Array in die Funktion überträgst und das Ergebnis protokollierst. Versuche außerdem, calculateHypotenuse mit einem Array aufzurufen, das nicht genau zwei Zahlen enthält, um das Type-Checking von TypeScript zu beobachten.
// 2 Als Nächstes definierst du eine Funktion namens calculateHypotenuse, die zwei Zahlen als Argumente akzeptiert und die Hypotenuse mithilfe der Formel √(a² + b²) berechnet. Du solltest diese Funktion aufrufen, indem du ein Array mit zwei Zahlen mit Hilfe der Spread-Syntax ausbreitest. (Tipp: Verwende Math.sqrt())
// console.log(sumAll([1, 2, 3, 4, 5]));
// console.log(sumAll(1, 2, 3, 4, 5));
// 3 Teste schließlich die Funktion sumAll, indem du sie mit mehreren Zahlen aufrufst und das Ergebnis protokollierst. Teste die Funktion calculateHypotenuse, indem du ein Array mit zwei Zahlen erstellst, dieses Array in die Funktion überträgst und das Ergebnis protokollierst. Versuche außerdem, calculateHypotenuse mit einem Array aufzurufen, das nicht genau zwei Zahlen enthält, um das Type-Checking von TypeScript zu beobachten.
// Example usage of sumAll
// Define total
const sumNums = sumAll(1, 2, 3, 4, 5);
console.log(sumNums); // => 15
console.log(sumAll()); // => 0
// Example usage of calculateHypotenuse with spread syntax
// Define hypotenuse
console.log(calculateHypotenuse(3, 4)); // => 5
// Attempting to call calculateHypotenuse with incorrect number of arguments
const invalidSides: [number, number] = [3, 4];
const invalidSides2: number[] = [3, 4, 5];
// Define invalidHypotenuse to call calculateHypotenuse with invalidSides => TypeScript Error: Expected 2 arguments, but got 3.
calculateHypotenuse(...invalidSides);
// calculateHypotenuse(...invalidSides2); // A spread argument must either have a tuple type or be passed to a rest parameter.
})();

View File

@@ -1,19 +1,36 @@
// Example usage of displayUserInfo
displayUserInfo({ name: 'John' }); // => Hello, John! You are 25 years old.
displayUserInfo({ name: 'Alice', age: 30 }); // => Hello, Alice! You are 30 years old.
(() => {
// Übung 24: Parameter Destrukturierung
// Example usage of getFirstTwoElements
getFirstTwoElements([5, 10]); // => First: 5, Second: 10
getFirstTwoElements([8]); // => First: 8, Second: undefined
// In dieser Übung übst du die Anwendung der Parameterdestrukturierung in TypeScript, indem du zwei einfache Funktionen erstellst. Die erste Funktion destrukturiert einen Objektparameter und verwendet einen Standardwert für eine seiner Eigenschaften. Die zweite Funktion destrukturiert einen Array-Parameter. Beide Funktionen enthalten Type Annotations, um sicherzustellen, dass sie die richtige Struktur erhalten.
// Übung 24: Parameter Destrukturierung
// Anweisungen:
// In dieser Übung übst du die Anwendung der Parameterdestrukturierung in TypeScript, indem du zwei einfache Funktionen erstellst. Die erste Funktion destrukturiert einen Objektparameter und verwendet einen Standardwert für eine seiner Eigenschaften. Die zweite Funktion destrukturiert einen Array-Parameter. Beide Funktionen enthalten Type Annotations, um sicherzustellen, dass sie die richtige Struktur erhalten.
// 1 Erstelle eine Funktion displayUserInfo, die die Eigenschaften name und age aus einem Objektparameter destrukturiert und einen Standardwert für age liefert.
// Anweisungen:
// interface User {
type User = {
name: string;
age?: number;
};
// 1 Erstelle eine Funktion displayUserInfo, die die Eigenschaften name und age aus einem Objektparameter destrukturiert und einen Standardwert für age liefert.
const displayUserInfo = (user: User): void => {
const { name, age = 18 } = user;
// 2 Erstelle eine Funktion getFirstTwoElements, die die ersten beiden Elemente aus einem Array-Parameter zerstört.
console.log(`Hello, ${name}! You are ${age} years old`);
};
// 3 Teste deine Funktionen, indem du sie mit den richtigen Argumenten aufrufst.
// 2 Erstelle eine Funktion getFirstTwoElements, die die ersten beiden Elemente aus einem Array-Parameter zerstört.
const getFirstTwoElements = ([firstEl, secondEL]: [number, number?]): void => {
console.log(`First: ${firstEl}, Second: ${secondEL}`);
};
// 3 Teste deine Funktionen, indem du sie mit den richtigen Argumenten aufrufst.
// Example usage of displayUserInfo
displayUserInfo({ name: 'John' }); // => Hello, John! You are 25 years old.
displayUserInfo({ name: 'Alice', age: 30 }); // => Hello, Alice! You are 30 years old.
// Example usage of getFirstTwoElements
getFirstTwoElements([5, 10]); // => First: 5, Second: 10
getFirstTwoElements([8]); // => First: 8, Second: undefined
})();

View File

@@ -1,17 +1,69 @@
// Übung 25: Optionale und readonly Eigenschaften
(() => {
// Übung 25: Optionale und readonly Eigenschaften
// In dieser Übung erstellst du eine interface für eine Product, die erforderliche, optionale und readonly Eigenschaften enthält. Anschließend schreibst du eine Funktion, die Informationen über das Produkt protokolliert und dabei optionale Eigenschaften korrekt behandelt.
// In dieser Übung erstellst du eine interface für eine Product, die erforderliche, optionale und readonly Eigenschaften enthält. Anschließend schreibst du eine Funktion, die Informationen über das Produkt protokolliert und dabei optionale Eigenschaften korrekt behandelt.
// Anweisungen:
// Anweisungen:
// 1 Definiere ein Product Interface, die Folgendes beinhaltet:
// 1 Definiere ein Product Interface, die Folgendes beinhaltet:
// Erforderliche Eigenschaften id (Number) und name (String).
// Optionale Eigenschaften price (Number) und description (String).
// Eine readonly category (String) Eigenschaft.
// Erforderliche Eigenschaften id (Number) und name (String).
// Optionale Eigenschaften price (Number) und description (String).
// Eine readonly category (String) Eigenschaft.
// 2 Schreibe eine Funktion printProductDetails, die ein Product Objekt annimmt und:
interface Product {
id: number;
name: string;
price?: number;
description?: string;
readonly category: string;
}
// Druckt die name, category und id.
// Wenn price undefined ist, drucke "Price: Not available".
// Wenn description undefined ist, drucke "No description availabler".
// 2 Schreibe eine Funktion printProductDetails, die ein Product Objekt annimmt und:
// Druckt die name, category und id.
// Wenn price undefined ist, drucke "Price: Not available".
// Wenn description undefined ist, drucke "No description availabler".
const printProductDetails = (product: Product): void => {
// let price = product.price ? product.price : 'Not avaible';
// let descr = product.description ? product.description : 'No description avaible';
let price = product.price ?? 'Not avaible';
let descr = product.description ?? 'No description avaible';
console.log(
`Name: ${product.name}, Kategorie: ${product.category}, ID: ${product.id}, Preis: ${price}, Beschreibung: ${descr}`,
);
};
//Aus Faulheit mit KI Objekte zum testen erstellt
// 1. Produkt mit ALLEN Eigenschaften
const fullProduct: Product = {
id: 101,
name: 'Gaming Laptop',
category: 'Elektronik',
price: 1299.99,
description: 'Leistungsstarker Laptop für Gaming und Arbeit.',
};
// 2. Produkt OHNE optionale Eigenschaften (weder Preis noch Beschreibung)
const minimalProduct: Product = {
id: 102,
name: 'Kaffeetasse',
category: 'Haushalt',
};
// 3. Produkt NUR MIT Beschreibung (ohne Preis)
const noPriceProduct: Product = {
id: 103,
name: 'Antikes Buch',
category: 'Bücher',
description: 'Ein rares Sammlerstück aus dem 19. Jahrhundert.',
};
// Tests ausführen:
printProductDetails(fullProduct); //Name: Gaming Laptop, Kategorie: Elektronik, ID: 101, Preis: 1299.99, Beschreibung: Leistungsstarker Laptop für Gaming und Arbeit.
printProductDetails(minimalProduct); //Name: Kaffeetasse, Kategorie: Haushalt, ID: 102, Preis: Not avaible, Beschreibung: No description avaible
printProductDetails(noPriceProduct); //Name: Antikes Buch, Kategorie: Bücher, ID: 103, Preis: Not avaible, Beschreibung: Ein rares Sammlerstück aus dem 19. Jahrhundert.
})();

View File

@@ -1,19 +1,41 @@
// Übung 26: Eine verschachtelte Objekteigenschaft aktualisieren
(() => {
// Übung 26: Eine verschachtelte Objekteigenschaft aktualisieren
// In dieser Übung definierst du ein Interface mit readonly und veränderbaren Eigenschaften und aktualisierst ein verschachteltes Objekt innerhalb des Interfaces. Du übst den Umgang mit verschachtelten Objekten und readonly Eigenschaften.
// In dieser Übung definierst du ein Interface mit readonly und veränderbaren Eigenschaften und aktualisierst ein verschachteltes Objekt innerhalb des Interfaces. Du übst den Umgang mit verschachtelten Objekten und readonly Eigenschaften.
// Anweisungen:
// Anweisungen:
// Erstelle ein Employee Interface, die Folgendes enthält:
// Erstelle ein Employee Interface, die Folgendes enthält:
// Eine readonly employeeId (Number) Eigenschaft.
// Eine veränderbare Detail-Eigenschaft, die ein Objekt ist, das Folgendes enthält: position (String), salary (Number)
// Eine readonly employeeId (Number) Eigenschaft.
interface Employee {
readonly employeeId: number;
details: {
position: string;
salary: number;
};
}
// Eine veränderbare Detail-Eigenschaft, die ein Objekt ist, das Folgendes enthält: position (String), salary (Number)
// Schreibe eine Funktion updateEmployeePosition die:
// Schreibe eine Funktion updateEmployeePosition die:
// Akzeptiert ein Employee Objekt und einen neuen position String.
const updateEmployeePosition = (emp: Employee, pos: string) => {
emp.details.position = pos;
console.log(`ID: ${emp.employeeId}, Position: ${emp.details.position}, Salaray: ${emp.details.salary}`);
};
// Akzeptiert ein Employee Objekt und einen neuen position String.
// Aktualisiert die position des Mitarbeiters, verhindert aber jede Änderung der employeeId.
// Aktualisiert die position des Mitarbeiters, verhindert aber jede Änderung der employeeId.
// Teste die Funktion, indem du ein Employee-Objekt erstellst und die Position aktualisierst.
// Teste die Funktion, indem du ein Employee-Objekt erstellst und die Position aktualisierst.
const employee: Employee = {
employeeId: 1,
details: {
position: 'normaler Angestellter',
salary: 1400,
},
};
updateEmployeePosition(employee, 'cooler Angestellter');
})();

View File

@@ -5,21 +5,42 @@
// Anweisungen:
// 1 Definiere ein Car Interface, die Folgendes beinhaltet:
// Erforderliche Eigenschaften make (string) und model (string).
// Optionale Eigenschaften year (Number) und color (String).
// Eine readonly vin (string) Eigenschaft (Fahrzeugidentifikationsnummer).
interface Car {
make: string;
model: string;
year?: number;
color?: string;
readonly vin: string;
}
// 2 Schreibe eine Funktion createCar die:
// Nimmt make, model und vin auf und gibt ein Car Objekt zurück.
// Optional können year und color akzeptiert werden.
type CreateCarFn = (a: string, b: string, c: string, d?: number, e?: string) => Car;
// const createCar = (make: string, model: string, vin: string, year?: number, color?: string): Car => {
// return { make, model, vin, year, color };
// };
const createCar: CreateCarFn = (make, model, vin, year, color) => {
return { make, model, vin, year, color };
};
// 3 Schreibe eine weitere Funktion paintCar die:
// Akzeptiert ein Car Objekt und einen neuen color String.
// Aktualisiert die color des Fahrzeugs (falls vorhanden), verhindert aber jede Änderung an der vin.
const paintCar = (car: Car, color: string): Car => {
const res: Car = car;
res.color = color;
return res;
};
let car1 = createCar('VW', 'Golf', '1234', 2020, 'blau');
console.log(car1);
car1 = paintCar(car1, 'gelb');
console.log(car1);

View File

@@ -9,7 +9,18 @@
// - make: String
// - model: String
// - year: Number
interface Car {
make: string;
model: string;
year: number;
}
// 2 Schreibe eine Funktion printCarDetails, die ein Objekt vom Typ Car annimmt und die Details des Autos ausgibt (make, model und year).
const printCarDetails = (car: Car) => {
console.log(`Hersteller: ${car.make}`);
console.log(`Model: ${car.model}`);
console.log(`Baujahr: ${car.year}`);
};
// 3 Versuche, der Funktion ein Objekt zu übergeben, das eine zusätzliche Eigenschaft hat, z. B. color. Beobachte, wie TypeScript mit der überzähligen Eigenschaft umgeht und erkläre, warum der Fehler auftritt.
// printCarDetails({ make: 'VW', model: 'Golf', year: 2020, color: 'blue' });

View File

@@ -1,11 +1,37 @@
// Übung 29: Extending einer einzelnen Interface
{
// Übung 29: Extending einer einzelnen Interface
// In dieser Übung wirst du eine bestehende Product Interface erweitern, um einen spezifischeren Typ Electronics zu erstellen. Du wirst zusätzliche Eigenschaften für Elektronikprodukte definieren und ein Objekt des Typs Electronics erstellen.
// In dieser Übung wirst du eine bestehende Product Interface erweitern, um einen spezifischeren Typ Electronics zu erstellen. Du wirst zusätzliche Eigenschaften für Elektronikprodukte definieren und ein Objekt des Typs Electronics erstellen.
// Anweisungen:
// Anweisungen:
// 1 Definiere ein Product Interface mit den Eigenschaften id, name und price.
// 1 Definiere ein Product Interface mit den Eigenschaften id, name und price.
interface Product {
readonly id: number;
name: string;
price: number;
}
// 2 Erweitere die Product Interface um eine Electronics Interface und füge die Eigenschaften brand und warranty hinzu.
// 2 Erweitere die Product Interface um eine Electronics Interface und füge die Eigenschaften brand und warranty hinzu.
interface Electronics extends Product {
brand: string;
warranty: number;
}
// 3 Erstelle ein Objekt vom Typ Electronics.
// 3 Erstelle ein Objekt vom Typ Electronics.
const comp: Electronics = {
id: 1,
name: 'Computer',
price: 1499.99,
brand: 'Asus',
warranty: 24,
};
const konsole: Electronics = {
id: 2,
name: 'Steam',
price: 1039.99,
brand: 'Valve',
warranty: 24,
};
}

View File

@@ -1,11 +1,25 @@
// Übung 30: Einzigartige Eigenschaften zu einem Subtyp hinzufügen
{
// 1 Erweitere die Product Interface, um eine Furniture Interface zu erstellen.
interface Product {
readonly id: number;
name: string;
price: number;
}
// In dieser Übung wirst du die Product Interface erweitern, um einen Typ Furniture zu erstellen, der spezielle Eigenschaften für Möbel hinzufügt, wie material und dimensions.
// 2 Füge die Eigenschaften material und dimensions zu den Möbeln hinzu.
interface Furniture extends Product {
material: string;
dimensions: { length: number; width: number; height: number };
}
// Anweisungen:
// 3 Erstelle ein Objekt vom Typ Furniture, das ein Möbelstück darstellt.
const schrank: Furniture = {
id: 2,
name: 'Schrank',
price: 199.99,
material: 'Holz',
dimensions: { length: 0.7, width: 1.5, height: 1.7 },
};
// 1 Erweitere die Product Interface, um eine Furniture Interface zu erstellen.
// 2 Füge die Eigenschaften material und dimensions zu den Möbeln hinzu.
// 3 Erstelle ein Objekt vom Typ Furniture, das ein Möbelstück darstellt.
console.log(JSON.stringify(schrank, null, 2));
}

View File

@@ -1,9 +1,23 @@
// Übung 31: Generic Functions für Arrays
{
// Übung 31: Generic Functions für Arrays
// In dieser Übung erstellst du eine generische Funktion, die mit Arrays beliebigen Typs funktioniert. Du definierst eine Funktion, die das erste Element des Arrays zurückgibt und es TypeScript ermöglicht, den Elementtyp aus dem übergebenen Array abzuleiten.
// In dieser Übung erstellst du eine generische Funktion, die mit Arrays beliebigen Typs funktioniert. Du definierst eine Funktion, die das erste Element des Arrays zurückgibt und es TypeScript ermöglicht, den Elementtyp aus dem übergebenen Array abzuleiten.
// Anweisungen:
// Anweisungen:
// 1 Definiere eine generische Funktion getFirstElement, die ein Array beliebigen Typs annimmt und das erste Element oder undefined zurückgibt, wenn das Array leer ist.
// 1 Definiere eine generische Funktion getFirstElement, die ein Array beliebigen Typs annimmt und das erste Element oder undefined zurückgibt, wenn das Array leer ist.
const getFirstElement = <T>(arg: T[]): T => {
return arg[0];
};
// 2 Rufe die Funktion mit Arrays verschiedener Typen auf (number[], string[], etc.) und protokolliere das Ergebnis.
// 2 Rufe die Funktion mit Arrays verschiedener Typen auf (number[], string[], etc.) und protokolliere das Ergebnis.
console.log(getFirstElement<number>([1, 2, 3]));
console.log(getFirstElement<string>(['a', 'b', 'c']));
console.log(
getFirstElement<object>([
{ name: 'a', id: 1 },
{ name: 'b', id: 2 },
]),
);
console.log(getFirstElement([]));
}

View File

@@ -5,7 +5,7 @@ Dabei werden die Typen ihrer Eingänge (Parameter) und ihres Ausgangs (Rückgabe
*/
{
type MathFn = (x: number, y: number) => number; // Funktionstype-deklaration
type MathFn = (x: number, y: number) => number; // Funktionstyp-deklaration
const mathOperationFn = (fn: MathFn): number => {
return fn(4, 5);
@@ -15,6 +15,10 @@ Dabei werden die Typen ihrer Eingänge (Parameter) und ihres Ausgangs (Rückgabe
return a * b;
};
function multiplyFn(a: number, b: number): number {
return a * b;
}
const multiply2: MathFn = (a, b) => {
return a * b;
};
@@ -52,9 +56,9 @@ Dabei werden die Typen ihrer Eingänge (Parameter) und ihres Ausgangs (Rückgabe
console.log(message);
};
// RECOMMENDED
type LogFn = (param: string) => void;
// RECOMMENDED
const logMessage3: LogFn = (message) => {
console.log(message);
};

View File

@@ -39,6 +39,23 @@ Eine Call Signature ermöglicht es dir, die Art und Weise zu beschreiben, wie ei
console.log(multiply(25, 25)); //=> 625
//--------
type Person = {
name: string;
age: number;
walk: (speed?: number) => string;
};
const person: Person = {
name: 'John',
age: 23,
walk() {
return "i'm walking.";
},
};
console.log(person.walk());
// VORSICHT SONDERFALL in der Signatur bei describable Function
// Bei Describable Function : anstatt => für Rückgabe

View File

@@ -11,6 +11,7 @@ Destrukturierung vereinfacht deinen Code, indem du Werte direkt aus Objekten ode
console.log(`Hello, ${name}! You are ${age} years old.`);
};
// NEGATIV BEISPIEL - NOT RECOMMENDED
const greetUser3: (o: { name: string; age: number }) => void = (
{
name,
@@ -37,6 +38,17 @@ Destrukturierung vereinfacht deinen Code, indem du Werte direkt aus Objekten ode
greetUser4({ name: 'Alice', age: 25 });
// RECOMMENDED ALTERNATIVE
type User = { name: string; age: number };
const greetUser5 = (user: User) => {
const { name, age } = user;
console.log(`Hello, ${name}! You are ${age} years old.`);
};
greetUser5({ name: 'Alice', age: 25 });
// -----
const sum = ({ a, b, c }: { a: number; b: number; c: number }): void => {
console.log(a + b + c);

View File

@@ -3,7 +3,7 @@
In TypeScript gibt es drei spezielle Typen: never, unknown und void. Diese Typen werden in bestimmten Situationen verwendet, um bestimmte Verhaltensweisen zu modellieren.
*/
// - void wird für Funktionen verwendet, die keinen Wert zurückgeben, was bedeutet, dass die Funktion eine Aktion ausführt, ohne ein Ergebnis zu liefern. Es wird kein Ergebnis erwartet, dennoch können Ergebnisse zurückgegeben werden ohne Fehlermeldung
// - void wird für Funktionen verwendet, die keinen Wert zurückgeben, was bedeutet, dass die Funktion eine Aktion ausführt, ohne ein Ergebnis zu liefern. Es wird kein Ergebnis erwartet, dennoch können Ergebnisse zurückgegeben werden ohne Fehlermeldung, wenn void in einer "call Signature" (Funktionstyp-Deklaration verwendet wird)
// - unknown ist eine sicherere Alternative zu any und erfordert Type-Checking, bevor Operationen mit Werten unbekannten Typs durchgeführt werden, wodurch Laufzeitfehler vermieden werden.
@@ -16,9 +16,9 @@ In TypeScript gibt es drei spezielle Typen: never, unknown und void. Diese Typen
const logMessage = (message: string): void => {
console.log(message);
};
const logMessage2: LogFn = (message) => {
console.log(message);
return 'message';
};
logMessage('Hello, TypeScript!');
@@ -114,6 +114,7 @@ In TypeScript gibt es drei spezielle Typen: never, unknown und void. Diese Typen
return 'Handling a circle';
case 'square':
return 'Handling a square';
default:
// throw new Error("shape doesn't exist");
const _exhaustiveCheck: never = shape; // TypeScript error if a new shape is added

View File

@@ -48,6 +48,7 @@ In TypeScript kannst du die Modifizierer für Eigenschaften in Objekten verwende
// ?? operator => nullish coalescing
const printBookInfo = (book: Book) => {
const author = book.author ?? 'Unknown'; // Use "Unknown" if author is undefined
const author2 = book.author ? book.author : 'Unknown'; // Use "Unknown" if author is undefined
const pages = book.pages ?? 'Page count not available';
console.log(`Title: ${book.title}, Author: ${author}, Pages: ${pages}`);
};

View File

@@ -82,7 +82,7 @@
type StringNumberMap = Record<string, number>;
const numbers: StringNumberMap = {
one: 1,
true: 1,
two: 2,
three: 3,
};

View File

@@ -0,0 +1,5 @@
# Projektarbeit & Exkurs
- Abschlussquiz
- **Exkurs Webprojekt in TS mit KI Part I**
- 1:1 Meeting

View File

@@ -0,0 +1,6 @@
# Meeting Fr. 31.07.2026
- [ ] 14:20 - 14:30 Uhr Adel
- [ ] 14:30 - 14:40 Uhr Kahleel
- [ ] 14:40 - 14:50 Uhr Ersin
- [ ] 14:50 - 15:00 Uhr Andreas

View File

@@ -0,0 +1,8 @@
{
"workbench.colorCustomizations": {
"titleBar.activeForeground": "#333",
"titleBar.activeBackground": "#d0d06b",
"titleBar.inactiveForeground": "#ddd",
"titleBar.inactiveBackground": "#a4a464"
}
}

View File

@@ -0,0 +1,68 @@
# BROWSE Products
GET http://127.0.0.1:8000/api/products
###
# READ Product
GET http://127.0.0.1:8000/api/products/c38913d4-9524-461f-85d2-525241166e8e
###
# ADD Product
POST http://127.0.0.1:8000/api/products
Content-Type: application/json
{
"name": "Testproduct",
"price": 9.99
}
###
# UPDATE Product
PUT http://127.0.0.1:8000/api/products
Content-Type: application/json
{
"_id": "940c316b-518c-4a18-b1f6-328e010eef4d",
"name": "Testproduct Updated",
"price": 123.22
}
###
# DELETE Product
DELETE http://127.0.0.1:8000/api/products/6cbe4783-cfb5-4ed7-8196-9d6b55217de0
Content-Type: application/json
###
# RESET Products
PATCH http://127.0.0.1:8000/api/products/reset
Content-Type: application/json
{
"reset": true
}
###
# SAVE Products
PATCH http://127.0.0.1:8000/api/products/save
Content-Type: application/json
{
"save": true
}

View File

@@ -0,0 +1,38 @@
[
{
"_id": "6cbe4783-cfb5-4ed7-8196-9d6b55217de0",
"position": 1,
"name": "3Doodler 3D Printing Pen",
"price": 29.99
},
{
"_id": "c38913d4-9524-461f-85d2-525241166e8e",
"position": 2,
"name": "Powerstation 5- E. Maximus Chargus",
"price": 44.95
},
{
"_id": "cdc887bd-fb0e-4485-8516-0a3720c3bb72",
"position": 3,
"name": "8-Bit Legendary Hero Heat-Change Mug",
"price": 6.99
},
{
"_id": "bded2710-8db8-41a5-87af-269195eab449",
"position": 4,
"name": "16-Bit Legendary Hero Heat-Change Mug",
"price": 12.99
},
{
"_id": "3b3352c1-789d-4b4f-8a93-f05f44ece5a5",
"position": 5,
"name": "32-Bit Legendary Hero Heat-Change Mug",
"price": 18.99
},
{
"_id": "5af39d33-cbcd-42c7-a160-50084faa91ba",
"position": 6,
"name": "64-Bit Legendary Hero Heat-Change Mug",
"price": 21.99
}
]

View File

@@ -0,0 +1,38 @@
[
{
"_id": "6cbe4783-cfb5-4ed7-8196-9d6b55217de0",
"position": 1,
"name": "3Doodler 3D Printing Pen",
"price": 29.99
},
{
"_id": "c38913d4-9524-461f-85d2-525241166e8e",
"position": 2,
"name": "Powerstation 5- E. Maximus Chargus",
"price": 44.95
},
{
"_id": "cdc887bd-fb0e-4485-8516-0a3720c3bb72",
"position": 3,
"name": "8-Bit Legendary Hero Heat-Change Mug",
"price": 6.99
},
{
"_id": "bded2710-8db8-41a5-87af-269195eab449",
"position": 4,
"name": "16-Bit Legendary Hero Heat-Change Mug",
"price": 12.99
},
{
"_id": "3b3352c1-789d-4b4f-8a93-f05f44ece5a5",
"position": 5,
"name": "32-Bit Legendary Hero Heat-Change Mug",
"price": 18.99
},
{
"_id": "5af39d33-cbcd-42c7-a160-50084faa91ba",
"position": 6,
"name": "64-Bit Legendary Hero Heat-Change Mug",
"price": 21.99
}
]

View File

@@ -0,0 +1,113 @@
import fs from 'node:fs';
import { v4 as id } from 'uuid';
// Factoryfunction
const ProductModel = () => {
let products = [];
const load = () => {
try {
products = JSON.parse(fs.readFileSync('./data/products.json', 'utf-8'));
} catch (error) {
console.log('Something went wrong: ', error);
products = [];
}
};
const reset = () => {
try {
products = JSON.parse(fs.readFileSync('./data/products.bakup.json', 'utf-8'));
} catch (error) {
console.log('Something went wrong: ', error);
products = [];
}
};
const save = () => {
try {
fs.writeFileSync('./data/products.json', JSON.stringify(products, null, 2), 'utf-8');
} catch (error) {
console.log('Something went wrong:', error);
}
};
const getAll = () => {
return products;
};
const get = (id) => {
const product = products.find((product) => {
return product._id === id;
});
// Guard
if (!product) return null;
return product;
};
const add = (product) => {
const { name, price } = product;
if (!hasAllProps(product)) return false;
products.push({
_id: id(),
name,
price: Number(price.toFixed(2)),
position: products.length + 1,
});
return true;
};
const update = (product) => {
const foundIdx = products.findIndex((obj) => {
return obj._id === product._id;
});
// Guard
if (foundIdx === -1 || !hasAllProps(product)) return false;
products[foundIdx] = product;
return true;
};
const remove = (id) => {
const foundIdx = products.findIndex((obj) => {
return obj._id === id;
});
// Guard
if (foundIdx === -1) return false;
products.splice(foundIdx, 1);
return true;
};
/**
* hasAllProps - Überprüfung ob Product "name" und "price"
* @return boolean
**/
const hasAllProps = (product) => {
// return Object.hasOwn(product, 'name') && Object.hasOwn(product, 'price');
const keys = Object.keys(product); // => ['name', 'price']
return keys.includes('name') && keys.includes('price');
};
return {
// load: load
load,
save,
add,
getAll,
get,
update,
delete: remove,
reset,
};
};
export default ProductModel;

View File

@@ -0,0 +1,20 @@
{
"name": "website-backend",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "npx nodemon server.js",
"server": "npx nodemon server.js"
},
"keywords": [],
"type": "module",
"license": "ISC",
"dependencies": {
"chalk": "^5.6.2",
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"express": "^5.2.1",
"uuid": "^14.0.1"
}
}

View File

@@ -0,0 +1,158 @@
import express from 'express';
import color from 'chalk';
import cors from 'cors';
import dotenv from 'dotenv';
dotenv.config(); // Sensible Konfigurationsvariablen in .env Datei auslagern
// Model
import ProductModel from './model/ProductModel.js'; // Dateiendung nicht vergessen
const PORT = process.env.SERVER_PORT; // || 8000;
const HOST = process.env.SERVER_HOST; // || '127.0.0.1'; // 'localhost'
const BASE_URL = `http://${HOST}:${PORT}`;
const app = express();
const products = ProductModel();
const corsOptions = {
origin: ['http://localhost:3000', 'http://127.0.0.1:3000'],
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
};
// lade Produkte
products.load();
// Middleware
app.use(cors(corsOptions)); // Fängt alle HTTP-Requests vor den Routenabfragen ab.
app.use(express.json()); // HTTP-Request Body wird als JSON Objekt erwartet und geparsed wird. // app.use(bodyParser.json()) - Modul body-parser war in express 4 notwendig
app.use((req, res, next) => {
console.log(color.yellow('HTTP-Method: '), color.magenta(req.method));
next();
});
// Routes
app.get('/', (req, res) => {
// console.log(req.headers);
res.send('SERVER WORKS!'); // HTTP-Response wird gesendet.
});
app.get('/persons', (req, res) => {
// res.setHeader('Content-Type', 'application/json'); // nicht notwendig den Content-Type selber zu definieren, da das express automatisch zuweist.
const persons = [
{ firstName: 'John', lastName: 'Wick' },
{ firstName: 'Jane', lastName: 'Doe' },
{ firstName: 'Max', lastName: 'Mustermann' },
];
// res.send(JSON.stringify(persons)); // nicht notwendig JS Objekte in JSON Strings umzuwandeln, das das express automatisch umwandelt
res.send(persons);
});
// REST API ======
// Representational State Transfer (abgekürzt REST)
// GET - Daten Holen
// POST - Daten hinzufügen
// PUT/PATCH - Daten aktualisieren / Einen Datensatz aktualisieren
// DELETE - Daten löschen
// HTTP - Methode - POST
// (C)RUD - create - neues Produkt erstellen
// BRE(A)D - add
app.post('/api/products', (req, res) => {
const product = req.body;
if (!products.add(product)) {
return res.status(400).send({ msg: 'Could not add product', status: 400, success: false });
}
return res.send({ msg: 'Product added', status: 200, success: true, data: product });
});
// HTTP - Methode - GET
// C(R)UD - read
// (B)READ - browse
app.get('/api/products', (req, res) => {
const data = products.getAll();
return res.send(data);
});
// HTTP - Methode - GET
// C(R)UD - read
// B(R)EAD - read
app.get('/api/products/:id', (req, res) => {
const id = req.params.id; // routenparameter :id wird als Eigenschaft in params-Objekt abgelegt
const product = products.get(id);
if (!product) {
return res.status(400).send({ msg: 'Could not get product', status: 400, success: false });
}
return res.send(product);
});
// HTTP - Methode - PUT/PATCH
// CR(U)D - update
// BR(E)AD - edit
app.put('/api/products', (req, res) => {
const product = req.body;
const updated = products.update(product);
if (!updated) {
return res.status(400).send({ msg: 'Could not update product', status: 400, success: false });
}
return res.send({ msg: 'Product updated', status: 200, success: true, data: product });
});
// HTTP - Methode - PATCH
// reseten von Produkten
app.patch('/api/products/reset', (req, res) => {
const reset = req.body.reset;
if (!reset) {
return res.status(400).send({ msg: 'Could not reset products', status: 400, success: false });
}
products.reset();
return res.send({ msg: 'Products reseted', status: 200, success: true, data: reset });
});
// HTTP - Methode - PATCH
// speichern von Produkten
app.patch('/api/products/save', (req, res) => {
const save = req.body.save;
if (!save) {
return res.status(400).send({ msg: 'Could not save products', status: 400, success: false });
}
products.save();
return res.send({ msg: 'Products saved', status: 200, success: true, data: save });
});
// HTTP - Methode - DELETE
// CRU(D) - delete
// BREA(D) - delete
app.delete('/api/products/:id', (req, res) => {
const id = req.params.id;
if (!products.delete(id)) {
return res.status(400).send({ msg: 'Could not delete product', status: 400, success: false });
}
return res.send({ msg: 'Product deleted', status: 200, success: true, data: id });
});
// =========
app.listen(PORT, HOST, () => {
console.log(color.magenta(`🚀 Server is running at: ${BASE_URL}`));
console.log(color.yellow('CTRL + C to close.'));
});

View File

@@ -0,0 +1,19 @@
import fs from 'node:fs';
import { v4 as id } from 'uuid';
console.log(id()); // 97412ca6-c7a8-4868-a49b-3cf4c2258dd8
try {
const data = JSON.parse(fs.readFileSync('../data/products.json', 'utf-8'));
const products = data.map((product, index) => {
return { _id: product._id || id(), position: index + 1, ...product };
});
console.log(products);
fs.writeFileSync('../data/products.json', JSON.stringify(products, null, 2), 'utf-8');
console.log('File modified');
} catch (error) {
console.log(error);
}

View File

@@ -0,0 +1,28 @@
{
"name": "utils",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "utils",
"version": "1.0.0",
"dependencies": {
"uuid": "^14.0.1"
}
},
"node_modules/uuid": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz",
"integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist-node/bin/uuid"
}
}
}
}

View File

@@ -0,0 +1,14 @@
{
"name": "utils",
"version": "1.0.0",
"description": "",
"main": "json-modifier.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"type": "module",
"dependencies": {
"uuid": "^14.0.1"
}
}

View File

@@ -0,0 +1,8 @@
{
"workbench.colorCustomizations": {
"titleBar.activeForeground": "#333",
"titleBar.activeBackground": "#86cd8b",
"titleBar.inactiveForeground": "#ddd",
"titleBar.inactiveBackground": "#6fa973"
}
}

View File

@@ -0,0 +1,33 @@
# Aufgabe
- Nach Positionsveränderung (drop, move-up,move-down) sollen alle Positionen der Zeilen als Array festgehalten werden.
```js
const positions = [
{
_id: '6cbe4783-cfb5-4ed7-8196-9d6b55217de0',
position: 1,
},
{
_id: '6cbe4783-cfb5-4ed7-8196-9d6b55217de0',
position: 2,
},
{...}
]
```
- Positionsarray an Server zur Aktualisierung senden (PUT)
```js
'http://127.0.0.1:8000/api/products/positions';
```
- Server: Positionen abfangen und products Array aktualisieren
```js
//ProductModel
updatePosition() {...}
```

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 1613 1368" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1491.62,327.703C1541.78,287.116 1594.34,248.113 1644.54,207.788L1618.71,488.181L1461.89,715C1476.02,740.352 1490.29,765.63 1504.68,790.833C1510.26,800.644 1520.89,820.767 1526.87,829.305C1522.43,842.281 1517.85,861.574 1514.38,875.237L1481.26,875.044C1484.81,860.981 1488.21,846.879 1491.44,832.74C1482.68,815.587 1470.33,796.564 1460.45,779.711C1455.18,770.719 1443.35,749.908 1437.18,743.219C1431.97,737.568 1413.75,723.823 1406.86,718.38L1349.17,672.785C1340.15,665.605 1325.81,654.677 1317.65,646.931C1344.15,658.866 1370.79,670.495 1397.55,681.814C1408.37,686.436 1424.93,694.156 1435.63,697.917C1446.75,678.509 1459.42,658.824 1471.18,639.729L1535.44,535.72C1545.43,519.643 1561.06,495.702 1569.9,479.652C1573.42,473.259 1576.75,431.467 1577.96,421.486C1582.65,383.079 1587.83,344.732 1593.48,306.455C1572.91,321.935 1550.73,340.775 1530.53,357.115C1511.75,410.62 1494.1,470.148 1476.2,524.616L1447.71,611.181C1443.15,625.019 1436.57,643.125 1432.83,656.866L1431.54,651.473C1433.84,641.445 1435.28,631.347 1437.1,621.239C1453.43,530.598 1468.66,439.697 1486.96,349.43C1488.43,342.142 1489.83,334.923 1491.62,327.703Z" style="fill:rgb(25,23,24);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M916.43,207.704C961.199,241.308 1004.53,277.533 1048.89,311.731C1055.75,317.026 1063.3,322.84 1069.76,328.558C1084.91,402.681 1098.82,477.052 1111.5,551.638L1123.71,623.629C1124.84,630.024 1127.8,649.079 1129.5,654.246L1127.21,654.902L1064.01,461.07L1043.22,395.892C1039.36,383.693 1034.98,368.156 1030.22,356.723L966.963,305.869C973.851,345.434 977.161,389.979 983.271,429.912C984.436,437.525 987.443,470.367 989.214,475.458C993.012,486.381 1011.45,512.702 1018.59,524.428L1125.74,698.168C1162.02,682.956 1198.1,667.285 1233.99,651.16L1236.23,653.085L1159.8,713.519C1149.67,721.504 1131.52,734.758 1123.15,743.745C1119.95,747.171 1107.65,768.564 1104.75,773.47C1092.95,793.324 1081.27,813.256 1069.73,833.265C1072.83,845.948 1077.43,862.679 1079.73,875.203L1045.92,874.941C1042.48,860.188 1038.86,843.686 1034.67,829.252C1055.31,792.12 1079.95,751.725 1099.25,714.842L1000.82,573.175L965.228,521.882C957.797,511.147 949.566,500.6 943.536,489.005C939.585,481.409 918.918,241.006 916.43,207.704Z" style="fill:rgb(25,23,24);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1219.69,1043.31C1216.9,1055.11 1215.25,1071.34 1213.71,1083.51C1211.47,1101.52 1209.04,1119.52 1206.41,1137.48C1213.84,1129.75 1221.59,1120.73 1228.78,1112.67C1231.48,1113.88 1231.74,1114.1 1234.2,1115.86C1224.59,1135.46 1214.83,1154.98 1204.91,1174.42C1201.52,1181.1 1198.22,1187.99 1194.72,1194.6C1185.82,1211.39 1181.67,1229.44 1176.43,1247.66C1169.12,1273.61 1161.65,1299.52 1154.02,1325.38C1183.18,1357.1 1211.08,1390.67 1240.43,1422.03L1320.27,1422.15C1349.41,1390.02 1378.35,1357.69 1407.07,1325.19C1399.71,1300.58 1392.6,1275.89 1385.75,1251.13C1382.8,1240.65 1376.83,1217.65 1372.78,1208.49C1356.12,1170.8 1333.4,1132.86 1316.7,1095.31C1329.42,1109.42 1341.94,1123.71 1354.25,1138.19C1352.41,1113.22 1345.93,1068.33 1341.47,1043.62C1364.43,1071.12 1363.55,1075.68 1374.28,1109.56L1400.59,1194.79C1402.94,1182.51 1407.12,1168.91 1410.29,1156.72C1417.49,1129.01 1425.33,1101.5 1432.42,1073.75C1444.98,1069.98 1457.6,1066.39 1470.27,1062.97C1461.28,1104.6 1445.83,1150.13 1435.34,1191.93C1431.14,1208.63 1421.72,1235.52 1420.04,1252.56C1419.68,1256.27 1426.98,1280.67 1428.52,1286.09L1441.6,1333.45C1413.47,1364.5 1386.31,1396.69 1358.3,1427.87C1350.61,1436.42 1342.63,1445.37 1335.54,1454.4L1225.48,1454.07L1160.83,1380.6C1149.19,1367.45 1129.53,1346.5 1119.24,1332.85L1142.09,1251.77C1130.78,1209.15 1118.57,1167.25 1106.94,1124.76C1101.69,1105.59 1094.99,1082.4 1091.02,1063.09C1103.38,1067.19 1115.9,1070.76 1128.57,1073.78C1138.21,1113.81 1150.3,1154.52 1160.43,1194.92L1185.93,1112.1C1190.27,1098.12 1195.43,1080 1201.13,1066.93C1202.5,1063.8 1216.75,1046.89 1219.69,1043.31Z" style="fill:rgb(25,23,24);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M964.12,882.509L1203.79,882.435C1217.38,902.737 1235.33,933.285 1249.05,951.826C1240.58,972.332 1233.36,996.483 1222.71,1015.55C1221.53,1017.65 1214.19,1024.7 1212.12,1026.69C1201.69,1037.2 1191.13,1047.59 1180.46,1057.85C1171.71,1059.38 1138.99,1066.24 1131.78,1064.48C1105.56,1058.1 1076.73,1049.63 1050.67,1042.53C1031.66,1005.96 1009.14,961.987 989.029,926.505C980.644,924.78 972.242,923.139 963.824,921.582C963.805,908.9 963.526,895.083 964.12,882.509Z" style="fill:rgb(25,23,24);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1363.24,882.527C1413.82,881.563 1464.41,883.283 1514.99,882.818C1542.28,882.568 1569.55,882.616 1596.84,883.007C1596.99,895.794 1597.02,908.583 1596.92,921.372C1588.42,923.144 1579.8,925.134 1571.31,927.006C1555.33,960.56 1531.67,1001.9 1513.92,1035.13L1510.06,1042.61C1484.37,1050.37 1455.74,1058.27 1429.75,1064.06C1423.09,1065.55 1387.96,1060.57 1382.29,1057.05C1369.21,1048.92 1351.36,1027.59 1338.82,1017C1328.5,996.763 1320.31,972.735 1312.25,951.658C1325.56,930.572 1341.46,904.817 1355.99,884.15C1357.42,882.113 1360.47,882.626 1363.24,882.527Z" style="fill:rgb(25,23,24);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1105.1,451.694C1123.75,471.025 1143.51,493.81 1161.62,513.944L1244.79,605.476C1268.06,605.928 1292.65,605.542 1316,605.557C1329.24,590.043 1345.43,573.176 1359.32,557.815L1454.77,452.524L1426.18,548.115C1423.29,557.767 1414.33,590.084 1410.7,596.764C1401.5,613.659 1385.71,636.678 1374.75,653.088C1382.9,622.923 1391.82,593.845 1400.53,563.928C1377.46,588.467 1353.94,614.808 1330.97,639.762C1318.71,637.771 1286.42,630.483 1276.22,631.425C1266.71,632.305 1240.73,637.673 1230.02,639.563C1208.22,616.643 1187.58,592.215 1165.29,569.201L1163.39,573.58C1170.4,593.856 1181.29,631.819 1186.17,652.325C1179.34,642.071 1153.56,606.025 1150.41,596.668C1135.2,551.472 1118.33,497.113 1105.1,451.694Z" style="fill:rgb(25,23,24);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1163.39,573.58C1162.86,571.669 1161.12,567.514 1161.62,565.744C1162.01,564.355 1165.14,568.964 1165.29,569.201L1163.39,573.58Z" style="fill:rgb(229,228,227);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1228.3,712.716L1177.25,821.977C1187.76,836.671 1198.67,850.77 1209,865.713C1219.45,874.636 1226.47,894.227 1208.18,869.09C1199.88,869.404 1154.72,870.247 1149.17,868.342C1130.51,858.443 1107.39,843.86 1088.66,832.959C1099.14,826.626 1117.98,817.48 1126.89,811.044C1136.44,804.133 1154.5,785.529 1163.71,776.506C1185.38,755.383 1206.91,734.12 1228.3,712.716Z" style="fill:rgb(25,23,24);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1209,865.713C1219.45,874.636 1226.47,894.227 1208.18,869.09C1199.88,869.404 1154.72,870.247 1149.17,868.342C1158.75,867.532 1205.19,870.377 1209,865.713Z" style="fill:rgb(52,50,51);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1333.39,713.392C1355.47,734.234 1377.91,756.725 1399.46,778.251C1425.23,804.008 1438.73,816.721 1472.12,832.344C1459.85,839.726 1419.58,862.166 1410.8,869.15C1397.53,869.311 1364.74,870.385 1352.74,869.192C1352.23,867.922 1351.89,867.068 1351.28,865.837C1362.05,851.206 1372.91,836.647 1383.87,822.161C1367.04,786.412 1349.28,749.469 1333.39,713.392Z" style="fill:rgb(25,23,24);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1239.83,891.5C1271.44,890.865 1306.06,891.718 1337.84,892.013C1328,906.67 1318.61,921.864 1308.88,936.658C1304.91,936.732 1294.23,927.336 1284.14,926.618C1270.4,925.64 1262.97,929.46 1252.44,937.566C1242.42,924.036 1231.87,906.324 1222.26,891.847L1239.83,891.5Z" style="fill:rgb(25,23,24);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1202.02,1241.87C1202.21,1241.04 1202.19,1240.46 1202.8,1239.98C1204.73,1239.69 1204.01,1248.97 1203.98,1250.27L1205.5,1284.24C1220.65,1301.02 1235.98,1317.63 1251.51,1334.06C1252.7,1341.96 1257,1356.91 1259.14,1365.16C1236.64,1351.28 1203.31,1326.48 1180.94,1310.91C1187.57,1287.58 1195.36,1265.02 1202.02,1241.87Z" style="fill:rgb(25,23,24);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1202.02,1241.87C1202.21,1241.04 1202.19,1240.46 1202.8,1239.98C1204.73,1239.69 1204.01,1248.97 1203.98,1250.27C1203.33,1247.32 1202.84,1244.8 1202.02,1241.87Z" style="fill:rgb(130,128,128);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1356.86,1249.9C1356.84,1248.26 1356.41,1240.46 1357.66,1239.59C1359.06,1239.84 1359.37,1243.13 1359.63,1244.35C1366.65,1266.61 1373.49,1288.16 1379.59,1310.68C1355.02,1329.17 1326.3,1347.75 1300.87,1365.76C1304.13,1355.65 1307.39,1344.41 1309.37,1333.97C1323.65,1319 1342.48,1299.34 1355.63,1283.73C1356.07,1273.66 1357.07,1259.67 1356.86,1249.9Z" style="fill:rgb(25,23,24);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1356.86,1249.9C1356.84,1248.26 1356.41,1240.46 1357.66,1239.59C1359.06,1239.84 1359.37,1243.13 1359.63,1244.35C1357.36,1244.59 1357.5,1247.71 1356.86,1249.9Z" style="fill:rgb(105,102,103);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1228.78,1112.67C1234.41,1106.63 1239.26,1100.54 1245.16,1094.34L1234.2,1115.86C1231.74,1114.1 1231.48,1113.88 1228.78,1112.67Z" style="fill:rgb(52,50,51);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1351.28,865.837C1351.89,867.068 1352.23,867.922 1352.74,869.192C1350.09,871.683 1344.39,880.569 1342.97,880.88C1336.3,882.343 1350.41,866.703 1351.28,865.837Z" style="fill:rgb(52,50,51);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1432.83,656.866C1432.61,657.516 1431.42,662.192 1430.5,662.165C1429.39,660.699 1431.14,653.359 1431.54,651.473L1432.83,656.866Z" style="fill:rgb(105,102,103);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1129.5,654.246C1129.73,655.393 1131.07,660.919 1129.98,661.699C1128.43,661.495 1127.52,656.217 1127.21,654.902L1129.5,654.246Z" style="fill:rgb(105,102,103);fill-rule:nonzero;"/>
</g>
<g transform="matrix(1,0,0,1,-506,-152)">
<path d="M1233.99,651.16C1238.94,648.18 1242.71,647.988 1236.23,653.085L1233.99,651.16Z" style="fill:rgb(52,50,51);fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,5 @@
[
{ "name": "3Doodler 3D Printing Pen", "price": 29.99 },
{ "name": "Powerstation 5- E. Maximus Chargus", "price": 44.95 },
{ "name": "8-Bit Legendary Hero Heat-Change Mug", "price": 6.99 }
]

View File

@@ -0,0 +1,49 @@
// import 'bootstrap'; // ohne Pfadangabe (Modul auslesen aus node_modules) funktioniert nur mit JS-Bundler (esbuild, rollup & co.)
import ProductManager from './modules/ProductManager'; // Dateiendung kann weggelassen werden, wenn ein Bundler eingesetzt wird (esbuild, webpack, rollup, etc.)
import Splide from '@splidejs/splide';
(() => {
// === DOM & VARS =======
const DOM = {};
// === INIT =============
const init = () => {
console.log('init');
router();
};
// === EVENTHANDLER =====
// === XHR/FETCH ========
// === FUNCTIONS ========
const router = () => {
const path = window.location.pathname;
console.log(path);
switch (path) {
case '/':
case '/index.html':
console.log('HOME');
const splider = new Splide('.splide', {
type: 'loop',
fixedHeight: '100%',
height: '100%',
autoHeight: true,
}).mount();
// nur Skripte, die für Home relevant ausführen
break;
case '/projects/product-manager.html':
console.log('PRODUCT_MANAGER');
const pm = ProductManager();
break;
default:
console.warn('Page not found');
}
};
init();
})();

View File

@@ -0,0 +1,574 @@
import { Modal } from 'bootstrap'; // ohne Pfadangabe (Modul auslesen aus node_modules) funktioniert nur mit JS-Bundler (esbuild, rollup & co.)
import Toast from './Toast';
// import * as bootstrap from 'bootstrap';
// import 'bootstrap';
// import 'https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js'; // würde auch ohne Bundler funktionieren
// import '../../../node_modules/bootstrap/dist/js/bootstrap.esm.js'; // würde auch ohne Bundler funktionieren
// Factory Function
const ProductManager = (el = null) => {
// === DOM & VARS =======
const BASE_URL = 'http://127.0.0.1:8000';
const module = el || document.querySelector('.product-manager') || console.error('Product Manager not found');
if (!module) return;
const DOM = {
module,
table: module.querySelector('.table-products'),
tBody: module.querySelector('tbody'),
inputName: module.querySelector('.input-product-name'),
inputPrice: module.querySelector('.input-product-price'),
btnAdd: module.querySelector('.button-product-add'),
templateRow: module.querySelector('.template-row'),
// Nav Actions
btnSave: module.querySelector('.button-products-save'),
btnReset: module.querySelector('.button-products-reset'),
// Modal Edit
modalEdit: module.querySelector('.modal-edit'),
formEdit: module.querySelector('.form-product-edit'),
inputEditName: module.querySelector('.input-edit-name'),
inputEditPrice: module.querySelector('.input-edit-price'),
inputEditId: module.querySelector('.input-edit-id'),
inputEditPosition: module.querySelector('.input-edit-position'),
btnUpdate: module.querySelector('.button-product-update'),
// Modal Delete
modalDelete: module.querySelector('.modal-delete'),
productName: module.querySelector('strong.product-name'),
btnConfirmDelete: module.querySelector('.button-confirm-delete'),
};
console.log(DOM);
const bsModalEdit = new Modal(DOM.modalEdit, {
backdrop: 'static', // true
keyboard: true,
});
const bsModalDelete = new Modal(DOM.modalDelete, {
backdrop: 'static', // true
keyboard: true,
});
// === INIT =============
const init = () => {
console.log('init');
// Funktionaufrufe zu beginn der Anwendung
initProducts();
// Event-Lauscher zu beginn der Anwendung
DOM.btnReset.addEventListener('click', onClickReset);
DOM.btnSave.addEventListener('click', onClickSave);
DOM.btnAdd.addEventListener('click', onClickAdd);
DOM.btnAdd.disabled = true;
DOM.btnConfirmDelete.addEventListener('click', onClickConfirmDelete);
DOM.formEdit.addEventListener('submit', onSubmitEdit);
window.addEventListener('keyup', onKeyUp);
};
// === EVENTHANDLER =====
const onKeyUp = (e) => {
// if (DOM.inputName.value === '' && DOM.inputPrice.value === '') {
// DOM.btnAdd.disabled = true;
// } else {
// DOM.btnAdd.disabled = false;
// }
DOM.btnAdd.disabled = (DOM.inputName.value === '' || DOM.inputPrice.value === ''); // prettier-ignore
};
const onClickReset = (e) => {
const btnEl = e.currentTarget;
// async process
resetProducts().then((data) => {
if (data.success) {
Toast(data.msg, 'success').show();
loadProducts();
}
});
};
const onClickSave = (e) => {
const btnEl = e.currentTarget;
const icon = btnEl.querySelector('i');
icon.classList.add('fa-jello');
// async process
saveProducts().then((data) => {
if (data.success) {
setTimeout(() => {
icon.classList.remove('fa-jello');
}, 500);
Toast(data.msg, 'success').show();
}
});
};
const onSubmitEdit = (e) => {
e.preventDefault(); // Standardverhalten unterbinden (Formular nicht an action versenden)
// console.log(Object.entries(new FormData(DOM.formEdit)));
const product = {
name: DOM.inputEditName.value,
price: Number(DOM.inputEditPrice.value),
_id: DOM.inputEditId.value,
position: DOM.inputEditPosition.value,
};
// update async process
DOM.btnUpdate.querySelector('i').classList.add('fa-spin');
updateProduct(product).then((data) => {
console.log(data);
if (data.success) {
Toast(data.msg, 'success').show();
DOM.btnUpdate.querySelector('i').classList.remove('fa-spin');
loadProducts(); // All Produkte auslesen
resetFields();
bsModalEdit.hide();
} else {
Toast(data.msg, 'error').show();
}
});
};
const onClickAdd = (e) => {
console.log('click');
const product = {
name: DOM.inputName.value,
price: Number(DOM.inputPrice.value),
};
createProduct(product).then((data) => {
console.log(data);
if (data.success) {
console.log('add');
Toast(data.msg, 'success').show();
loadProducts();
resetFields();
}
});
// addProduct(product);
disableNonFunctionalButtons();
};
const onClickEdit = (e) => {
const btnEl = e.currentTarget;
const currentRow = parents(btnEl, 'tr')[0];
const id = currentRow.dataset.id;
// async fetch
getProduct(id).then((data) => {
// console.log(data);
showModalEdit(data);
});
};
const onClickRemove = (e) => {
const btnEl = e.currentTarget;
const currentRow = parents(btnEl, 'tr')[0];
const id = currentRow.dataset.id;
const productName = currentRow.querySelector('.td-name').textContent.trim();
DOM.btnConfirmDelete.dataset.id = id;
DOM.productName.textContent = productName;
// async fetch
// deleteProduct(id).then((data) => {
// if (data.success) {
// Toast(data.msg, 'success').show();
// console.log('delete: success', data);
// loadProducts();
// } else {
// Toast(data.msg, 'error').show();
// console.error(data);
// }
// });
};
const onClickConfirmDelete = (e) => {
const btnEl = e.currentTarget;
const id = btnEl.dataset.id;
// async fetch
deleteProduct(id).then((data) => {
if (data.success) {
Toast(data.msg, 'success').show();
bsModalDelete.hide();
loadProducts();
} else {
Toast(data.msg, 'error').show();
console.error(data);
}
});
};
const onClickMoveUp = (e) => {
const btnEl = e.currentTarget;
// const currentRowEl = btnEl.parentNode.parentNode; // aktuelle Zeile (tr) mit dem Button
const currentRowEl = parents(btnEl, 'tr')[0];
// ParentNode.insertBefore(referenceElement, targetElement)
// DOM.tBody.insertBefore(currentRowEl, currentRowEl.previousElementSibling);
// targetElement.before(referenceElement)
currentRowEl.previousElementSibling.before(currentRowEl);
console.log('move up');
disableNonFunctionalButtons();
};
const onClickMoveDown = (e) => {
const btnEl = e.currentTarget;
// const currentRowEl = btnEl.parentNode.parentNode; // aktuelle Zeile (tr) mit dem Button
const currentRowEl = parents(btnEl, 'tr')[0];
// ParentNode.insertBefore(referenceElement, targetElement)
// DOM.tBody.insertBefore(currentRowEl, currentRowEl.nextElementSibling.nextElementSibling);
// targetElement.after(referenceElement)
currentRowEl.nextElementSibling.after(currentRowEl);
console.log('move down');
disableNonFunctionalButtons();
};
// Drag 'n Drop EventHandler
const onMouseDownDrag = (e) => {
const btnEl = e.currentTarget;
const currentRowEl = parents(btnEl, 'tr')[0];
currentRowEl.draggable = true;
};
const onMouseUpDrag = (e) => {
const btnEl = e.currentTarget;
const currentRowEl = parents(btnEl, 'tr')[0];
currentRowEl.draggable = false;
};
const onDragStart = (e) => {
console.log('DragEvent: ', e);
const currentRowEl = e.currentTarget; // tr <- aktuelle Zeie, die bewegt wird
const idx = [...currentRowEl.parentNode.children].indexOf(currentRowEl);
e.dataTransfer.setData('text/plain', idx);
// e.dataTransfer.setData('application/json', JSON.stringify({idx, name: sourceElement}));
console.log('tr index:', idx);
};
const onDragOver = (e) => {
e.preventDefault(); // WICHTIG! Sonst funktioniert drop EventHandler nicht.
};
const onDrop = (e) => {
const currentRowEl = e.currentTarget; // tr <- Zielzeile, auf die gedroppt wird.
const targetIdx = [...currentRowEl.parentNode.children].indexOf(currentRowEl);
const sourceIdx = parseInt(e.dataTransfer.getData('text/plain'));
// const { sourceIdx } = JSON.parse(e.dataTransfer.getData('application/json'));
console.log({ targetIdx, sourceIdx });
// Guard wenn Element auf sich selbst losgelassen wurde, abbruch
if (sourceIdx === targetIdx) {
return;
}
const draggedEl = DOM.tBody.querySelector(`tr:nth-child(${sourceIdx + 1})`);
// const draggedEl = DOM.tBody.children[sourceIdx]
// Verschiebe das Element im DOM
if (sourceIdx > targetIdx) {
currentRowEl.before(draggedEl);
} else {
currentRowEl.after(draggedEl);
}
disableNonFunctionalButtons();
};
// === XHR/FETCH ========
// HTTP - Methode - PATCH
// reset products
const resetProducts = async () => {
try {
const response = await fetch(`${BASE_URL}/api/products/reset`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ reset: true }),
}); // HTTP Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response
return data;
} catch (error) {
Toast(error, 'error').show();
console.error(error);
return error;
}
};
// HTTP - Methode - PATCH
// save products
const saveProducts = async () => {
try {
const response = await fetch(`${BASE_URL}/api/products/save`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ save: true }),
}); // HTTP Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response
return data;
} catch (error) {
Toast(error, 'error').show();
console.error(error);
return error;
}
};
// HTTP - Methode - POST
// (C)RUD - create
// BRE(A)D - add - neues Produkt hinzufügen
const createProduct = async (product) => {
try {
const response = await fetch(`${BASE_URL}/api/products`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(product),
}); // HTTP Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response
return data;
} catch (error) {
Toast(error, 'error').show();
console.error(error);
return error;
}
};
// HTTP - Methode - GET
// C(R)UD - Read
// (B)READ - Browse - auslesen aller Produkte
const getProducts = async () => {
try {
const response = await fetch(`${BASE_URL}/api/products`); // HTTP Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response
return data;
} catch (error) {
console.error(error);
return error;
}
};
// HTTP - Methode - GET
// C(R)UD - Read
// B(R)EAD - Read - auslesen eines Produkts
const getProduct = async (id) => {
try {
const response = await fetch(`${BASE_URL}/api/products/${id}`); // HTTP Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response
return data;
} catch (error) {
console.error(error);
return error;
}
};
// HTTP - Methode - PUT
// CR(U)D - update
// BR(E)AD - edit - Produkt aktualisieren
const updateProduct = async (product) => {
try {
const response = await fetch(`${BASE_URL}/api/products`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(product),
}); // HTTP Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json(); // HTTP-Response
return data;
} catch (error) {
console.error(error);
return error;
}
};
// HTTP - Methode - DELETE
// CRU(D) - delete
// BREA(D) - delete - Produkt löschen
const deleteProduct = async (id) => {
try {
const response = await fetch(`${BASE_URL}/api/products/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
}); // HTTP Request URL
if (!response.ok) {
throw new Error('Fetch went wrong.');
}
const data = await response.json();
return data;
} catch (error) {
console.error(error);
return error;
}
};
// === FUNCTIONS ========
const initProducts = () => {
getProducts().then((products) => {
products.forEach((product) => {
addProduct(product);
});
Toast('Init products').show();
disableNonFunctionalButtons();
});
// PRODUCTS.forEach((product) => {
// addProduct(product);
// });
//disableNonFunctionalButtons();
// PRODUCTS.forEach(addProduct);
};
const loadProducts = () => {
DOM.tBody.innerHTML = '';
getProducts().then((data) => {
data.forEach((product) => {
addProduct(product);
});
disableNonFunctionalButtons();
});
};
const addProduct = (product) => {
const { name, price, _id: id, position } = product;
// https://developer.mozilla.org/de/docs/Web/API/HTMLTemplateElement/content
const trEl = DOM.templateRow.content.firstElementChild.cloneNode(true);
const tdNameEl = trEl.querySelector('.td-name');
const tdPriceEl = trEl.querySelector('.td-price');
const btnRemoveEl = trEl.querySelector('.button-product-remove');
const btnMoveUpEl = trEl.querySelector('.button-product-move-up');
const btnMoveDownEl = trEl.querySelector('.button-product-move-down');
const btnDragEl = trEl.querySelector('.button-drag');
const btnEditEl = trEl.querySelector('.button-product-edit');
tdNameEl.textContent = name;
tdPriceEl.textContent = price;
btnEditEl.addEventListener('click', onClickEdit);
btnRemoveEl.addEventListener('click', onClickRemove);
btnMoveUpEl.addEventListener('click', onClickMoveUp);
btnMoveDownEl.addEventListener('click', onClickMoveDown);
// Drag 'n Drop Events
btnDragEl.addEventListener('mousedown', onMouseDownDrag);
btnDragEl.addEventListener('mouseup', onMouseUpDrag);
trEl.addEventListener('dragstart', onDragStart);
trEl.addEventListener('dragover', onDragOver);
trEl.addEventListener('drop', onDrop);
trEl.dataset.id = id; // <tr data-id="..." >...</tr>
trEl.dataset.position = position; // <tr data-position="..." >...</tr>
DOM.tBody.appendChild(trEl); // Im DOM hinzufügen
};
const showModalEdit = (product) => {
const { name = '', price = 0, _id: id, position } = product;
DOM.inputEditName.value = name;
DOM.inputEditPrice.value = price;
DOM.inputEditId.value = id;
DOM.inputEditPosition.value = position;
};
const resetFields = () => {
DOM.btnAdd.disabled = true;
DOM.inputPrice.value = '';
DOM.inputName.value = '';
DOM.formEdit.reset();
};
const disableNonFunctionalButtons = () => {
Array.from(DOM.tBody.querySelectorAll('tr')).forEach((tr) => {
const btnMoveUp = tr.querySelector('.button-product-move-up');
const btnMoveDown = tr.querySelector('.button-product-move-down');
btnMoveUp.disabled = !tr.previousElementSibling;
btnMoveDown.disabled = !tr.nextElementSibling;
});
};
// Helferfunktion aus jQuery (youmightnotjquery)
const parents = (el, selector) => {
const parents = [];
while ((el = el.parentNode) && el !== document) {
if (!selector || el.matches(selector)) parents.push(el);
}
return parents;
};
init();
return {
init,
initProducts,
resetFields,
};
};
// Freigabe für import
export default ProductManager;

View File

@@ -0,0 +1,59 @@
import Toastify from 'toastify-js'; // das Modul wird von node_modules ausgelesen und gebundlet. Funktioniert nur mit JS-Bundler
const Toast = (text = '', variant = 'primary') => {
const getColorByName = (variant = '') => {
let color;
switch (variant.trim().toLowerCase()) {
case 'primary':
color = '#0d6efd';
break;
case 'secondary':
color = '#6c757d';
break;
case 'warning':
color = '#ffc720';
break;
case 'success':
color = '#13653f';
break;
case 'info':
color = '#25cff2';
break;
case 'error':
case 'danger':
color = '#a52834';
break;
case 'light':
color = '#babbbc';
break;
case 'dark':
color = '#373b3e';
break;
default:
console.warn('variant not found');
color: 'white';
}
return color;
};
const show = () => {
Toastify({
text: text,
duration: 2000,
className: variant,
gravity: 'top', // `top` or `bottom`
position: 'center', // `left`, `center` or `right`
stopOnFocus: true, // Prevents dismissing of toast on hover
style: {
background: getColorByName(variant),
},
}).showToast();
};
return {
show,
};
};
export default Toast;

View File

@@ -0,0 +1,3 @@
// @import 'bootstrap-5.3.8/bootstrap-grid';
@import 'bootstrap-5.3.8/bootstrap';
// @import 'bulma-1.04/index'

View File

@@ -0,0 +1,153 @@
//
// Base styles
//
.accordion {
// scss-docs-start accordion-css-vars
--#{$prefix}accordion-color: #{$accordion-color};
--#{$prefix}accordion-bg: #{$accordion-bg};
--#{$prefix}accordion-transition: #{$accordion-transition};
--#{$prefix}accordion-border-color: #{$accordion-border-color};
--#{$prefix}accordion-border-width: #{$accordion-border-width};
--#{$prefix}accordion-border-radius: #{$accordion-border-radius};
--#{$prefix}accordion-inner-border-radius: #{$accordion-inner-border-radius};
--#{$prefix}accordion-btn-padding-x: #{$accordion-button-padding-x};
--#{$prefix}accordion-btn-padding-y: #{$accordion-button-padding-y};
--#{$prefix}accordion-btn-color: #{$accordion-button-color};
--#{$prefix}accordion-btn-bg: #{$accordion-button-bg};
--#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon)};
--#{$prefix}accordion-btn-icon-width: #{$accordion-icon-width};
--#{$prefix}accordion-btn-icon-transform: #{$accordion-icon-transform};
--#{$prefix}accordion-btn-icon-transition: #{$accordion-icon-transition};
--#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon)};
--#{$prefix}accordion-btn-focus-box-shadow: #{$accordion-button-focus-box-shadow};
--#{$prefix}accordion-body-padding-x: #{$accordion-body-padding-x};
--#{$prefix}accordion-body-padding-y: #{$accordion-body-padding-y};
--#{$prefix}accordion-active-color: #{$accordion-button-active-color};
--#{$prefix}accordion-active-bg: #{$accordion-button-active-bg};
// scss-docs-end accordion-css-vars
}
.accordion-button {
position: relative;
display: flex;
align-items: center;
width: 100%;
padding: var(--#{$prefix}accordion-btn-padding-y) var(--#{$prefix}accordion-btn-padding-x);
@include font-size($font-size-base);
color: var(--#{$prefix}accordion-btn-color);
text-align: left; // Reset button style
background-color: var(--#{$prefix}accordion-btn-bg);
border: 0;
@include border-radius(0);
overflow-anchor: none;
@include transition(var(--#{$prefix}accordion-transition));
&:not(.collapsed) {
color: var(--#{$prefix}accordion-active-color);
background-color: var(--#{$prefix}accordion-active-bg);
box-shadow: inset 0 calc(-1 * var(--#{$prefix}accordion-border-width)) 0 var(--#{$prefix}accordion-border-color); // stylelint-disable-line function-disallowed-list
&::after {
background-image: var(--#{$prefix}accordion-btn-active-icon);
transform: var(--#{$prefix}accordion-btn-icon-transform);
}
}
// Accordion icon
&::after {
flex-shrink: 0;
width: var(--#{$prefix}accordion-btn-icon-width);
height: var(--#{$prefix}accordion-btn-icon-width);
margin-left: auto;
content: "";
background-image: var(--#{$prefix}accordion-btn-icon);
background-repeat: no-repeat;
background-size: var(--#{$prefix}accordion-btn-icon-width);
@include transition(var(--#{$prefix}accordion-btn-icon-transition));
}
&:hover {
z-index: 2;
}
&:focus {
z-index: 3;
outline: 0;
box-shadow: var(--#{$prefix}accordion-btn-focus-box-shadow);
}
}
.accordion-header {
margin-bottom: 0;
}
.accordion-item {
color: var(--#{$prefix}accordion-color);
background-color: var(--#{$prefix}accordion-bg);
border: var(--#{$prefix}accordion-border-width) solid var(--#{$prefix}accordion-border-color);
&:first-of-type {
@include border-top-radius(var(--#{$prefix}accordion-border-radius));
> .accordion-header .accordion-button {
@include border-top-radius(var(--#{$prefix}accordion-inner-border-radius));
}
}
&:not(:first-of-type) {
border-top: 0;
}
// Only set a border-radius on the last item if the accordion is collapsed
&:last-of-type {
@include border-bottom-radius(var(--#{$prefix}accordion-border-radius));
> .accordion-header .accordion-button {
&.collapsed {
@include border-bottom-radius(var(--#{$prefix}accordion-inner-border-radius));
}
}
> .accordion-collapse {
@include border-bottom-radius(var(--#{$prefix}accordion-border-radius));
}
}
}
.accordion-body {
padding: var(--#{$prefix}accordion-body-padding-y) var(--#{$prefix}accordion-body-padding-x);
}
// Flush accordion items
//
// Remove borders and border-radius to keep accordion items edge-to-edge.
.accordion-flush {
> .accordion-item {
border-right: 0;
border-left: 0;
@include border-radius(0);
&:first-child { border-top: 0; }
&:last-child { border-bottom: 0; }
// stylelint-disable selector-max-class
> .accordion-collapse,
> .accordion-header .accordion-button,
> .accordion-header .accordion-button.collapsed {
@include border-radius(0);
}
// stylelint-enable selector-max-class
}
}
@if $enable-dark-mode {
@include color-mode(dark) {
.accordion-button::after {
--#{$prefix}accordion-btn-icon: #{escape-svg($accordion-button-icon-dark)};
--#{$prefix}accordion-btn-active-icon: #{escape-svg($accordion-button-active-icon-dark)};
}
}
}

View File

@@ -0,0 +1,68 @@
//
// Base styles
//
.alert {
// scss-docs-start alert-css-vars
--#{$prefix}alert-bg: transparent;
--#{$prefix}alert-padding-x: #{$alert-padding-x};
--#{$prefix}alert-padding-y: #{$alert-padding-y};
--#{$prefix}alert-margin-bottom: #{$alert-margin-bottom};
--#{$prefix}alert-color: inherit;
--#{$prefix}alert-border-color: transparent;
--#{$prefix}alert-border: #{$alert-border-width} solid var(--#{$prefix}alert-border-color);
--#{$prefix}alert-border-radius: #{$alert-border-radius};
--#{$prefix}alert-link-color: inherit;
// scss-docs-end alert-css-vars
position: relative;
padding: var(--#{$prefix}alert-padding-y) var(--#{$prefix}alert-padding-x);
margin-bottom: var(--#{$prefix}alert-margin-bottom);
color: var(--#{$prefix}alert-color);
background-color: var(--#{$prefix}alert-bg);
border: var(--#{$prefix}alert-border);
@include border-radius(var(--#{$prefix}alert-border-radius));
}
// Headings for larger alerts
.alert-heading {
// Specified to prevent conflicts of changing $headings-color
color: inherit;
}
// Provide class for links that match alerts
.alert-link {
font-weight: $alert-link-font-weight;
color: var(--#{$prefix}alert-link-color);
}
// Dismissible alerts
//
// Expand the right padding and account for the close button's positioning.
.alert-dismissible {
padding-right: $alert-dismissible-padding-r;
// Adjust close link position
.btn-close {
position: absolute;
top: 0;
right: 0;
z-index: $stretched-link-z-index + 1;
padding: $alert-padding-y * 1.25 $alert-padding-x;
}
}
// scss-docs-start alert-modifiers
// Generate contextual modifier classes for colorizing the alert
@each $state in map-keys($theme-colors) {
.alert-#{$state} {
--#{$prefix}alert-color: var(--#{$prefix}#{$state}-text-emphasis);
--#{$prefix}alert-bg: var(--#{$prefix}#{$state}-bg-subtle);
--#{$prefix}alert-border-color: var(--#{$prefix}#{$state}-border-subtle);
--#{$prefix}alert-link-color: var(--#{$prefix}#{$state}-text-emphasis);
}
}
// scss-docs-end alert-modifiers

View File

@@ -0,0 +1,38 @@
// Base class
//
// Requires one of the contextual, color modifier classes for `color` and
// `background-color`.
.badge {
// scss-docs-start badge-css-vars
--#{$prefix}badge-padding-x: #{$badge-padding-x};
--#{$prefix}badge-padding-y: #{$badge-padding-y};
@include rfs($badge-font-size, --#{$prefix}badge-font-size);
--#{$prefix}badge-font-weight: #{$badge-font-weight};
--#{$prefix}badge-color: #{$badge-color};
--#{$prefix}badge-border-radius: #{$badge-border-radius};
// scss-docs-end badge-css-vars
display: inline-block;
padding: var(--#{$prefix}badge-padding-y) var(--#{$prefix}badge-padding-x);
@include font-size(var(--#{$prefix}badge-font-size));
font-weight: var(--#{$prefix}badge-font-weight);
line-height: 1;
color: var(--#{$prefix}badge-color);
text-align: center;
white-space: nowrap;
vertical-align: baseline;
@include border-radius(var(--#{$prefix}badge-border-radius));
@include gradient-bg();
// Empty badges collapse automatically
&:empty {
display: none;
}
}
// Quick fix for badges in buttons
.btn .badge {
position: relative;
top: -1px;
}

View File

@@ -0,0 +1,40 @@
.breadcrumb {
// scss-docs-start breadcrumb-css-vars
--#{$prefix}breadcrumb-padding-x: #{$breadcrumb-padding-x};
--#{$prefix}breadcrumb-padding-y: #{$breadcrumb-padding-y};
--#{$prefix}breadcrumb-margin-bottom: #{$breadcrumb-margin-bottom};
@include rfs($breadcrumb-font-size, --#{$prefix}breadcrumb-font-size);
--#{$prefix}breadcrumb-bg: #{$breadcrumb-bg};
--#{$prefix}breadcrumb-border-radius: #{$breadcrumb-border-radius};
--#{$prefix}breadcrumb-divider-color: #{$breadcrumb-divider-color};
--#{$prefix}breadcrumb-item-padding-x: #{$breadcrumb-item-padding-x};
--#{$prefix}breadcrumb-item-active-color: #{$breadcrumb-active-color};
// scss-docs-end breadcrumb-css-vars
display: flex;
flex-wrap: wrap;
padding: var(--#{$prefix}breadcrumb-padding-y) var(--#{$prefix}breadcrumb-padding-x);
margin-bottom: var(--#{$prefix}breadcrumb-margin-bottom);
@include font-size(var(--#{$prefix}breadcrumb-font-size));
list-style: none;
background-color: var(--#{$prefix}breadcrumb-bg);
@include border-radius(var(--#{$prefix}breadcrumb-border-radius));
}
.breadcrumb-item {
// The separator between breadcrumbs (by default, a forward-slash: "/")
+ .breadcrumb-item {
padding-left: var(--#{$prefix}breadcrumb-item-padding-x);
&::before {
float: left; // Suppress inline spacings and underlining of the separator
padding-right: var(--#{$prefix}breadcrumb-item-padding-x);
color: var(--#{$prefix}breadcrumb-divider-color);
content: var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider)) #{"/* rtl:"} var(--#{$prefix}breadcrumb-divider, escape-svg($breadcrumb-divider-flipped)) #{"*/"};
}
}
&.active {
color: var(--#{$prefix}breadcrumb-item-active-color);
}
}

View File

@@ -0,0 +1,147 @@
// Make the div behave like a button
.btn-group,
.btn-group-vertical {
position: relative;
display: inline-flex;
vertical-align: middle; // match .btn alignment given font-size hack above
> .btn {
position: relative;
flex: 1 1 auto;
}
// Bring the hover, focused, and "active" buttons to the front to overlay
// the borders properly
> .btn-check:checked + .btn,
> .btn-check:focus + .btn,
> .btn:hover,
> .btn:focus,
> .btn:active,
> .btn.active {
z-index: 1;
}
}
// Optional: Group multiple button groups together for a toolbar
.btn-toolbar {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
.input-group {
width: auto;
}
}
.btn-group {
@include border-radius($btn-border-radius);
// Prevent double borders when buttons are next to each other
> :not(.btn-check:first-child) + .btn,
> .btn-group:not(:first-child) {
margin-left: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list
}
// Reset rounded corners
> .btn:not(:last-child):not(.dropdown-toggle),
> .btn.dropdown-toggle-split:first-child,
> .btn-group:not(:last-child) > .btn {
@include border-end-radius(0);
}
// The left radius should be 0 if the button is:
// - the "third or more" child
// - the second child and the previous element isn't `.btn-check` (making it the first child visually)
// - part of a btn-group which isn't the first child
> .btn:nth-child(n + 3),
> :not(.btn-check) + .btn,
> .btn-group:not(:first-child) > .btn {
@include border-start-radius(0);
}
}
// Sizing
//
// Remix the default button sizing classes into new ones for easier manipulation.
.btn-group-sm > .btn { @extend .btn-sm; }
.btn-group-lg > .btn { @extend .btn-lg; }
//
// Split button dropdowns
//
.dropdown-toggle-split {
padding-right: $btn-padding-x * .75;
padding-left: $btn-padding-x * .75;
&::after,
.dropup &::after,
.dropend &::after {
margin-left: 0;
}
.dropstart &::before {
margin-right: 0;
}
}
.btn-sm + .dropdown-toggle-split {
padding-right: $btn-padding-x-sm * .75;
padding-left: $btn-padding-x-sm * .75;
}
.btn-lg + .dropdown-toggle-split {
padding-right: $btn-padding-x-lg * .75;
padding-left: $btn-padding-x-lg * .75;
}
// The clickable button for toggling the menu
// Set the same inset shadow as the :active state
.btn-group.show .dropdown-toggle {
@include box-shadow($btn-active-box-shadow);
// Show no shadow for `.btn-link` since it has no other button styles.
&.btn-link {
@include box-shadow(none);
}
}
//
// Vertical button groups
//
.btn-group-vertical {
flex-direction: column;
align-items: flex-start;
justify-content: center;
> .btn,
> .btn-group {
width: 100%;
}
> .btn:not(:first-child),
> .btn-group:not(:first-child) {
margin-top: calc(-1 * #{$btn-border-width}); // stylelint-disable-line function-disallowed-list
}
// Reset rounded corners
> .btn:not(:last-child):not(.dropdown-toggle),
> .btn-group:not(:last-child) > .btn {
@include border-bottom-radius(0);
}
// The top radius should be 0 if the button is:
// - the "third or more" child
// - the second child and the previous element isn't `.btn-check` (making it the first child visually)
// - part of a btn-group which isn't the first child
> .btn:nth-child(n + 3),
> :not(.btn-check) + .btn,
> .btn-group:not(:first-child) > .btn {
@include border-top-radius(0);
}
}

View File

@@ -0,0 +1,216 @@
//
// Base styles
//
.btn {
// scss-docs-start btn-css-vars
--#{$prefix}btn-padding-x: #{$btn-padding-x};
--#{$prefix}btn-padding-y: #{$btn-padding-y};
--#{$prefix}btn-font-family: #{$btn-font-family};
@include rfs($btn-font-size, --#{$prefix}btn-font-size);
--#{$prefix}btn-font-weight: #{$btn-font-weight};
--#{$prefix}btn-line-height: #{$btn-line-height};
--#{$prefix}btn-color: #{$btn-color};
--#{$prefix}btn-bg: transparent;
--#{$prefix}btn-border-width: #{$btn-border-width};
--#{$prefix}btn-border-color: transparent;
--#{$prefix}btn-border-radius: #{$btn-border-radius};
--#{$prefix}btn-hover-border-color: transparent;
--#{$prefix}btn-box-shadow: #{$btn-box-shadow};
--#{$prefix}btn-disabled-opacity: #{$btn-disabled-opacity};
--#{$prefix}btn-focus-box-shadow: 0 0 0 #{$btn-focus-width} rgba(var(--#{$prefix}btn-focus-shadow-rgb), .5);
// scss-docs-end btn-css-vars
display: inline-block;
padding: var(--#{$prefix}btn-padding-y) var(--#{$prefix}btn-padding-x);
font-family: var(--#{$prefix}btn-font-family);
@include font-size(var(--#{$prefix}btn-font-size));
font-weight: var(--#{$prefix}btn-font-weight);
line-height: var(--#{$prefix}btn-line-height);
color: var(--#{$prefix}btn-color);
text-align: center;
text-decoration: if($link-decoration == none, null, none);
white-space: $btn-white-space;
vertical-align: middle;
cursor: if($enable-button-pointers, pointer, null);
user-select: none;
border: var(--#{$prefix}btn-border-width) solid var(--#{$prefix}btn-border-color);
@include border-radius(var(--#{$prefix}btn-border-radius));
@include gradient-bg(var(--#{$prefix}btn-bg));
@include box-shadow(var(--#{$prefix}btn-box-shadow));
@include transition($btn-transition);
&:hover {
color: var(--#{$prefix}btn-hover-color);
text-decoration: if($link-hover-decoration == underline, none, null);
background-color: var(--#{$prefix}btn-hover-bg);
border-color: var(--#{$prefix}btn-hover-border-color);
}
.btn-check + &:hover {
// override for the checkbox/radio buttons
color: var(--#{$prefix}btn-color);
background-color: var(--#{$prefix}btn-bg);
border-color: var(--#{$prefix}btn-border-color);
}
&:focus-visible {
color: var(--#{$prefix}btn-hover-color);
@include gradient-bg(var(--#{$prefix}btn-hover-bg));
border-color: var(--#{$prefix}btn-hover-border-color);
outline: 0;
// Avoid using mixin so we can pass custom focus shadow properly
@if $enable-shadows {
box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow);
} @else {
box-shadow: var(--#{$prefix}btn-focus-box-shadow);
}
}
.btn-check:focus-visible + & {
border-color: var(--#{$prefix}btn-hover-border-color);
outline: 0;
// Avoid using mixin so we can pass custom focus shadow properly
@if $enable-shadows {
box-shadow: var(--#{$prefix}btn-box-shadow), var(--#{$prefix}btn-focus-box-shadow);
} @else {
box-shadow: var(--#{$prefix}btn-focus-box-shadow);
}
}
.btn-check:checked + &,
:not(.btn-check) + &:active,
&:first-child:active,
&.active,
&.show {
color: var(--#{$prefix}btn-active-color);
background-color: var(--#{$prefix}btn-active-bg);
// Remove CSS gradients if they're enabled
background-image: if($enable-gradients, none, null);
border-color: var(--#{$prefix}btn-active-border-color);
@include box-shadow(var(--#{$prefix}btn-active-shadow));
&:focus-visible {
// Avoid using mixin so we can pass custom focus shadow properly
@if $enable-shadows {
box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow);
} @else {
box-shadow: var(--#{$prefix}btn-focus-box-shadow);
}
}
}
.btn-check:checked:focus-visible + & {
// Avoid using mixin so we can pass custom focus shadow properly
@if $enable-shadows {
box-shadow: var(--#{$prefix}btn-active-shadow), var(--#{$prefix}btn-focus-box-shadow);
} @else {
box-shadow: var(--#{$prefix}btn-focus-box-shadow);
}
}
&:disabled,
&.disabled,
fieldset:disabled & {
color: var(--#{$prefix}btn-disabled-color);
pointer-events: none;
background-color: var(--#{$prefix}btn-disabled-bg);
background-image: if($enable-gradients, none, null);
border-color: var(--#{$prefix}btn-disabled-border-color);
opacity: var(--#{$prefix}btn-disabled-opacity);
@include box-shadow(none);
}
}
//
// Alternate buttons
//
// scss-docs-start btn-variant-loops
@each $color, $value in $theme-colors {
.btn-#{$color} {
@if $color == "light" {
@include button-variant(
$value,
$value,
$hover-background: shade-color($value, $btn-hover-bg-shade-amount),
$hover-border: shade-color($value, $btn-hover-border-shade-amount),
$active-background: shade-color($value, $btn-active-bg-shade-amount),
$active-border: shade-color($value, $btn-active-border-shade-amount)
);
} @else if $color == "dark" {
@include button-variant(
$value,
$value,
$hover-background: tint-color($value, $btn-hover-bg-tint-amount),
$hover-border: tint-color($value, $btn-hover-border-tint-amount),
$active-background: tint-color($value, $btn-active-bg-tint-amount),
$active-border: tint-color($value, $btn-active-border-tint-amount)
);
} @else {
@include button-variant($value, $value);
}
}
}
@each $color, $value in $theme-colors {
.btn-outline-#{$color} {
@include button-outline-variant($value);
}
}
// scss-docs-end btn-variant-loops
//
// Link buttons
//
// Make a button look and behave like a link
.btn-link {
--#{$prefix}btn-font-weight: #{$font-weight-normal};
--#{$prefix}btn-color: #{$btn-link-color};
--#{$prefix}btn-bg: transparent;
--#{$prefix}btn-border-color: transparent;
--#{$prefix}btn-hover-color: #{$btn-link-hover-color};
--#{$prefix}btn-hover-border-color: transparent;
--#{$prefix}btn-active-color: #{$btn-link-hover-color};
--#{$prefix}btn-active-border-color: transparent;
--#{$prefix}btn-disabled-color: #{$btn-link-disabled-color};
--#{$prefix}btn-disabled-border-color: transparent;
--#{$prefix}btn-box-shadow: 0 0 0 #000; // Can't use `none` as keyword negates all values when used with multiple shadows
--#{$prefix}btn-focus-shadow-rgb: #{$btn-link-focus-shadow-rgb};
text-decoration: $link-decoration;
@if $enable-gradients {
background-image: none;
}
&:hover,
&:focus-visible {
text-decoration: $link-hover-decoration;
}
&:focus-visible {
color: var(--#{$prefix}btn-color);
}
&:hover {
color: var(--#{$prefix}btn-hover-color);
}
// No need for an active state here
}
//
// Button Sizes
//
.btn-lg {
@include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg);
}
.btn-sm {
@include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm);
}

View File

@@ -0,0 +1,238 @@
//
// Base styles
//
.card {
// scss-docs-start card-css-vars
--#{$prefix}card-spacer-y: #{$card-spacer-y};
--#{$prefix}card-spacer-x: #{$card-spacer-x};
--#{$prefix}card-title-spacer-y: #{$card-title-spacer-y};
--#{$prefix}card-title-color: #{$card-title-color};
--#{$prefix}card-subtitle-color: #{$card-subtitle-color};
--#{$prefix}card-border-width: #{$card-border-width};
--#{$prefix}card-border-color: #{$card-border-color};
--#{$prefix}card-border-radius: #{$card-border-radius};
--#{$prefix}card-box-shadow: #{$card-box-shadow};
--#{$prefix}card-inner-border-radius: #{$card-inner-border-radius};
--#{$prefix}card-cap-padding-y: #{$card-cap-padding-y};
--#{$prefix}card-cap-padding-x: #{$card-cap-padding-x};
--#{$prefix}card-cap-bg: #{$card-cap-bg};
--#{$prefix}card-cap-color: #{$card-cap-color};
--#{$prefix}card-height: #{$card-height};
--#{$prefix}card-color: #{$card-color};
--#{$prefix}card-bg: #{$card-bg};
--#{$prefix}card-img-overlay-padding: #{$card-img-overlay-padding};
--#{$prefix}card-group-margin: #{$card-group-margin};
// scss-docs-end card-css-vars
position: relative;
display: flex;
flex-direction: column;
min-width: 0; // See https://github.com/twbs/bootstrap/pull/22740#issuecomment-305868106
height: var(--#{$prefix}card-height);
color: var(--#{$prefix}body-color);
word-wrap: break-word;
background-color: var(--#{$prefix}card-bg);
background-clip: border-box;
border: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color);
@include border-radius(var(--#{$prefix}card-border-radius));
@include box-shadow(var(--#{$prefix}card-box-shadow));
> hr {
margin-right: 0;
margin-left: 0;
}
> .list-group {
border-top: inherit;
border-bottom: inherit;
&:first-child {
border-top-width: 0;
@include border-top-radius(var(--#{$prefix}card-inner-border-radius));
}
&:last-child {
border-bottom-width: 0;
@include border-bottom-radius(var(--#{$prefix}card-inner-border-radius));
}
}
// Due to specificity of the above selector (`.card > .list-group`), we must
// use a child selector here to prevent double borders.
> .card-header + .list-group,
> .list-group + .card-footer {
border-top: 0;
}
}
.card-body {
// Enable `flex-grow: 1` for decks and groups so that card blocks take up
// as much space as possible, ensuring footers are aligned to the bottom.
flex: 1 1 auto;
padding: var(--#{$prefix}card-spacer-y) var(--#{$prefix}card-spacer-x);
color: var(--#{$prefix}card-color);
}
.card-title {
margin-bottom: var(--#{$prefix}card-title-spacer-y);
color: var(--#{$prefix}card-title-color);
}
.card-subtitle {
margin-top: calc(-.5 * var(--#{$prefix}card-title-spacer-y)); // stylelint-disable-line function-disallowed-list
margin-bottom: 0;
color: var(--#{$prefix}card-subtitle-color);
}
.card-text:last-child {
margin-bottom: 0;
}
.card-link {
&:hover {
text-decoration: if($link-hover-decoration == underline, none, null);
}
+ .card-link {
margin-left: var(--#{$prefix}card-spacer-x);
}
}
//
// Optional textual caps
//
.card-header {
padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x);
margin-bottom: 0; // Removes the default margin-bottom of <hN>
color: var(--#{$prefix}card-cap-color);
background-color: var(--#{$prefix}card-cap-bg);
border-bottom: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color);
&:first-child {
@include border-radius(var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius) 0 0);
}
}
.card-footer {
padding: var(--#{$prefix}card-cap-padding-y) var(--#{$prefix}card-cap-padding-x);
color: var(--#{$prefix}card-cap-color);
background-color: var(--#{$prefix}card-cap-bg);
border-top: var(--#{$prefix}card-border-width) solid var(--#{$prefix}card-border-color);
&:last-child {
@include border-radius(0 0 var(--#{$prefix}card-inner-border-radius) var(--#{$prefix}card-inner-border-radius));
}
}
//
// Header navs
//
.card-header-tabs {
margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
margin-bottom: calc(-1 * var(--#{$prefix}card-cap-padding-y)); // stylelint-disable-line function-disallowed-list
margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
border-bottom: 0;
.nav-link.active {
background-color: var(--#{$prefix}card-bg);
border-bottom-color: var(--#{$prefix}card-bg);
}
}
.card-header-pills {
margin-right: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
margin-left: calc(-.5 * var(--#{$prefix}card-cap-padding-x)); // stylelint-disable-line function-disallowed-list
}
// Card image
.card-img-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
padding: var(--#{$prefix}card-img-overlay-padding);
@include border-radius(var(--#{$prefix}card-inner-border-radius));
}
.card-img,
.card-img-top,
.card-img-bottom {
width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch
}
.card-img,
.card-img-top {
@include border-top-radius(var(--#{$prefix}card-inner-border-radius));
}
.card-img,
.card-img-bottom {
@include border-bottom-radius(var(--#{$prefix}card-inner-border-radius));
}
//
// Card groups
//
.card-group {
// The child selector allows nested `.card` within `.card-group`
// to display properly.
> .card {
margin-bottom: var(--#{$prefix}card-group-margin);
}
@include media-breakpoint-up(sm) {
display: flex;
flex-flow: row wrap;
// The child selector allows nested `.card` within `.card-group`
// to display properly.
> .card {
flex: 1 0 0;
margin-bottom: 0;
+ .card {
margin-left: 0;
border-left: 0;
}
// Handle rounded corners
@if $enable-rounded {
&:not(:last-child) {
@include border-end-radius(0);
> .card-img-top,
> .card-header {
// stylelint-disable-next-line property-disallowed-list
border-top-right-radius: 0;
}
> .card-img-bottom,
> .card-footer {
// stylelint-disable-next-line property-disallowed-list
border-bottom-right-radius: 0;
}
}
&:not(:first-child) {
@include border-start-radius(0);
> .card-img-top,
> .card-header {
// stylelint-disable-next-line property-disallowed-list
border-top-left-radius: 0;
}
> .card-img-bottom,
> .card-footer {
// stylelint-disable-next-line property-disallowed-list
border-bottom-left-radius: 0;
}
}
}
}
}
}

View File

@@ -0,0 +1,226 @@
// Notes on the classes:
//
// 1. .carousel.pointer-event should ideally be pan-y (to allow for users to scroll vertically)
// even when their scroll action started on a carousel, but for compatibility (with Firefox)
// we're preventing all actions instead
// 2. The .carousel-item-start and .carousel-item-end is used to indicate where
// the active slide is heading.
// 3. .active.carousel-item is the current slide.
// 4. .active.carousel-item-start and .active.carousel-item-end is the current
// slide in its in-transition state. Only one of these occurs at a time.
// 5. .carousel-item-next.carousel-item-start and .carousel-item-prev.carousel-item-end
// is the upcoming slide in transition.
.carousel {
position: relative;
}
.carousel.pointer-event {
touch-action: pan-y;
}
.carousel-inner {
position: relative;
width: 100%;
overflow: hidden;
@include clearfix();
}
.carousel-item {
position: relative;
display: none;
float: left;
width: 100%;
margin-right: -100%;
backface-visibility: hidden;
@include transition($carousel-transition);
}
.carousel-item.active,
.carousel-item-next,
.carousel-item-prev {
display: block;
}
.carousel-item-next:not(.carousel-item-start),
.active.carousel-item-end {
transform: translateX(100%);
}
.carousel-item-prev:not(.carousel-item-end),
.active.carousel-item-start {
transform: translateX(-100%);
}
//
// Alternate transitions
//
.carousel-fade {
.carousel-item {
opacity: 0;
transition-property: opacity;
transform: none;
}
.carousel-item.active,
.carousel-item-next.carousel-item-start,
.carousel-item-prev.carousel-item-end {
z-index: 1;
opacity: 1;
}
.active.carousel-item-start,
.active.carousel-item-end {
z-index: 0;
opacity: 0;
@include transition(opacity 0s $carousel-transition-duration);
}
}
//
// Left/right controls for nav
//
.carousel-control-prev,
.carousel-control-next {
position: absolute;
top: 0;
bottom: 0;
z-index: 1;
// Use flex for alignment (1-3)
display: flex; // 1. allow flex styles
align-items: center; // 2. vertically center contents
justify-content: center; // 3. horizontally center contents
width: $carousel-control-width;
padding: 0;
color: $carousel-control-color;
text-align: center;
background: none;
filter: var(--#{$prefix}carousel-control-icon-filter);
border: 0;
opacity: $carousel-control-opacity;
@include transition($carousel-control-transition);
// Hover/focus state
&:hover,
&:focus {
color: $carousel-control-color;
text-decoration: none;
outline: 0;
opacity: $carousel-control-hover-opacity;
}
}
.carousel-control-prev {
left: 0;
background-image: if($enable-gradients, linear-gradient(90deg, rgba($black, .25), rgba($black, .001)), null);
}
.carousel-control-next {
right: 0;
background-image: if($enable-gradients, linear-gradient(270deg, rgba($black, .25), rgba($black, .001)), null);
}
// Icons for within
.carousel-control-prev-icon,
.carousel-control-next-icon {
display: inline-block;
width: $carousel-control-icon-width;
height: $carousel-control-icon-width;
background-repeat: no-repeat;
background-position: 50%;
background-size: 100% 100%;
}
.carousel-control-prev-icon {
background-image: escape-svg($carousel-control-prev-icon-bg) #{"/*rtl:" + escape-svg($carousel-control-next-icon-bg) + "*/"};
}
.carousel-control-next-icon {
background-image: escape-svg($carousel-control-next-icon-bg) #{"/*rtl:" + escape-svg($carousel-control-prev-icon-bg) + "*/"};
}
// Optional indicator pips/controls
//
// Add a container (such as a list) with the following class and add an item (ideally a focusable control,
// like a button) with data-bs-target for each slide your carousel holds.
.carousel-indicators {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
display: flex;
justify-content: center;
padding: 0;
// Use the .carousel-control's width as margin so we don't overlay those
margin-right: $carousel-control-width;
margin-bottom: 1rem;
margin-left: $carousel-control-width;
[data-bs-target] {
box-sizing: content-box;
flex: 0 1 auto;
width: $carousel-indicator-width;
height: $carousel-indicator-height;
padding: 0;
margin-right: $carousel-indicator-spacer;
margin-left: $carousel-indicator-spacer;
text-indent: -999px;
cursor: pointer;
background-color: var(--#{$prefix}carousel-indicator-active-bg);
background-clip: padding-box;
border: 0;
// Use transparent borders to increase the hit area by 10px on top and bottom.
border-top: $carousel-indicator-hit-area-height solid transparent;
border-bottom: $carousel-indicator-hit-area-height solid transparent;
opacity: $carousel-indicator-opacity;
@include transition($carousel-indicator-transition);
}
.active {
opacity: $carousel-indicator-active-opacity;
}
}
// Optional captions
//
//
.carousel-caption {
position: absolute;
right: (100% - $carousel-caption-width) * .5;
bottom: $carousel-caption-spacer;
left: (100% - $carousel-caption-width) * .5;
padding-top: $carousel-caption-padding-y;
padding-bottom: $carousel-caption-padding-y;
color: var(--#{$prefix}carousel-caption-color);
text-align: center;
}
// Dark mode carousel
@mixin carousel-dark() {
--#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg-dark};
--#{$prefix}carousel-caption-color: #{$carousel-caption-color-dark};
--#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter-dark};
}
.carousel-dark {
@include carousel-dark();
}
:root,
[data-bs-theme="light"] {
--#{$prefix}carousel-indicator-active-bg: #{$carousel-indicator-active-bg};
--#{$prefix}carousel-caption-color: #{$carousel-caption-color};
--#{$prefix}carousel-control-icon-filter: #{$carousel-control-icon-filter};
}
@if $enable-dark-mode {
@include color-mode(dark, true) {
@include carousel-dark();
}
}

View File

@@ -0,0 +1,66 @@
// Transparent background and border properties included for button version.
// iOS requires the button element instead of an anchor tag.
// If you want the anchor version, it requires `href="#"`.
// See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile
.btn-close {
// scss-docs-start close-css-vars
--#{$prefix}btn-close-color: #{$btn-close-color};
--#{$prefix}btn-close-bg: #{ escape-svg($btn-close-bg) };
--#{$prefix}btn-close-opacity: #{$btn-close-opacity};
--#{$prefix}btn-close-hover-opacity: #{$btn-close-hover-opacity};
--#{$prefix}btn-close-focus-shadow: #{$btn-close-focus-shadow};
--#{$prefix}btn-close-focus-opacity: #{$btn-close-focus-opacity};
--#{$prefix}btn-close-disabled-opacity: #{$btn-close-disabled-opacity};
// scss-docs-end close-css-vars
box-sizing: content-box;
width: $btn-close-width;
height: $btn-close-height;
padding: $btn-close-padding-y $btn-close-padding-x;
color: var(--#{$prefix}btn-close-color);
background: transparent var(--#{$prefix}btn-close-bg) center / $btn-close-width auto no-repeat; // include transparent for button elements
filter: var(--#{$prefix}btn-close-filter);
border: 0; // for button elements
@include border-radius();
opacity: var(--#{$prefix}btn-close-opacity);
// Override <a>'s hover style
&:hover {
color: var(--#{$prefix}btn-close-color);
text-decoration: none;
opacity: var(--#{$prefix}btn-close-hover-opacity);
}
&:focus {
outline: 0;
box-shadow: var(--#{$prefix}btn-close-focus-shadow);
opacity: var(--#{$prefix}btn-close-focus-opacity);
}
&:disabled,
&.disabled {
pointer-events: none;
user-select: none;
opacity: var(--#{$prefix}btn-close-disabled-opacity);
}
}
@mixin btn-close-white() {
--#{$prefix}btn-close-filter: #{$btn-close-filter-dark};
}
.btn-close-white {
@include btn-close-white();
}
:root,
[data-bs-theme="light"] {
--#{$prefix}btn-close-filter: #{$btn-close-filter};
}
@if $enable-dark-mode {
@include color-mode(dark, true) {
@include btn-close-white();
}
}

View File

@@ -0,0 +1,41 @@
// Container widths
//
// Set the container width, and override it for fixed navbars in media queries.
@if $enable-container-classes {
// Single container class with breakpoint max-widths
.container,
// 100% wide container at all breakpoints
.container-fluid {
@include make-container();
}
// Responsive containers that are 100% wide until a breakpoint
@each $breakpoint, $container-max-width in $container-max-widths {
.container-#{$breakpoint} {
@extend .container-fluid;
}
@include media-breakpoint-up($breakpoint, $grid-breakpoints) {
%responsive-container-#{$breakpoint} {
max-width: $container-max-width;
}
// Extend each breakpoint which is smaller or equal to the current breakpoint
$extend-breakpoint: true;
@each $name, $width in $grid-breakpoints {
@if ($extend-breakpoint) {
.container#{breakpoint-infix($name, $grid-breakpoints)} {
@extend %responsive-container-#{$breakpoint};
}
// Once the current breakpoint is reached, stop extending
@if ($breakpoint == $name) {
$extend-breakpoint: false;
}
}
}
}
}
}

View File

@@ -0,0 +1,250 @@
// The dropdown wrapper (`<div>`)
.dropup,
.dropend,
.dropdown,
.dropstart,
.dropup-center,
.dropdown-center {
position: relative;
}
.dropdown-toggle {
white-space: nowrap;
// Generate the caret automatically
@include caret();
}
// The dropdown menu
.dropdown-menu {
// scss-docs-start dropdown-css-vars
--#{$prefix}dropdown-zindex: #{$zindex-dropdown};
--#{$prefix}dropdown-min-width: #{$dropdown-min-width};
--#{$prefix}dropdown-padding-x: #{$dropdown-padding-x};
--#{$prefix}dropdown-padding-y: #{$dropdown-padding-y};
--#{$prefix}dropdown-spacer: #{$dropdown-spacer};
@include rfs($dropdown-font-size, --#{$prefix}dropdown-font-size);
--#{$prefix}dropdown-color: #{$dropdown-color};
--#{$prefix}dropdown-bg: #{$dropdown-bg};
--#{$prefix}dropdown-border-color: #{$dropdown-border-color};
--#{$prefix}dropdown-border-radius: #{$dropdown-border-radius};
--#{$prefix}dropdown-border-width: #{$dropdown-border-width};
--#{$prefix}dropdown-inner-border-radius: #{$dropdown-inner-border-radius};
--#{$prefix}dropdown-divider-bg: #{$dropdown-divider-bg};
--#{$prefix}dropdown-divider-margin-y: #{$dropdown-divider-margin-y};
--#{$prefix}dropdown-box-shadow: #{$dropdown-box-shadow};
--#{$prefix}dropdown-link-color: #{$dropdown-link-color};
--#{$prefix}dropdown-link-hover-color: #{$dropdown-link-hover-color};
--#{$prefix}dropdown-link-hover-bg: #{$dropdown-link-hover-bg};
--#{$prefix}dropdown-link-active-color: #{$dropdown-link-active-color};
--#{$prefix}dropdown-link-active-bg: #{$dropdown-link-active-bg};
--#{$prefix}dropdown-link-disabled-color: #{$dropdown-link-disabled-color};
--#{$prefix}dropdown-item-padding-x: #{$dropdown-item-padding-x};
--#{$prefix}dropdown-item-padding-y: #{$dropdown-item-padding-y};
--#{$prefix}dropdown-header-color: #{$dropdown-header-color};
--#{$prefix}dropdown-header-padding-x: #{$dropdown-header-padding-x};
--#{$prefix}dropdown-header-padding-y: #{$dropdown-header-padding-y};
// scss-docs-end dropdown-css-vars
position: absolute;
z-index: var(--#{$prefix}dropdown-zindex);
display: none; // none by default, but block on "open" of the menu
min-width: var(--#{$prefix}dropdown-min-width);
padding: var(--#{$prefix}dropdown-padding-y) var(--#{$prefix}dropdown-padding-x);
margin: 0; // Override default margin of ul
@include font-size(var(--#{$prefix}dropdown-font-size));
color: var(--#{$prefix}dropdown-color);
text-align: left; // Ensures proper alignment if parent has it changed (e.g., modal footer)
list-style: none;
background-color: var(--#{$prefix}dropdown-bg);
background-clip: padding-box;
border: var(--#{$prefix}dropdown-border-width) solid var(--#{$prefix}dropdown-border-color);
@include border-radius(var(--#{$prefix}dropdown-border-radius));
@include box-shadow(var(--#{$prefix}dropdown-box-shadow));
&[data-bs-popper] {
top: 100%;
left: 0;
margin-top: var(--#{$prefix}dropdown-spacer);
}
@if $dropdown-padding-y == 0 {
> .dropdown-item:first-child,
> li:first-child .dropdown-item {
@include border-top-radius(var(--#{$prefix}dropdown-inner-border-radius));
}
> .dropdown-item:last-child,
> li:last-child .dropdown-item {
@include border-bottom-radius(var(--#{$prefix}dropdown-inner-border-radius));
}
}
}
// scss-docs-start responsive-breakpoints
// We deliberately hardcode the `bs-` prefix because we check
// this custom property in JS to determine Popper's positioning
@each $breakpoint in map-keys($grid-breakpoints) {
@include media-breakpoint-up($breakpoint) {
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
.dropdown-menu#{$infix}-start {
--bs-position: start;
&[data-bs-popper] {
right: auto;
left: 0;
}
}
.dropdown-menu#{$infix}-end {
--bs-position: end;
&[data-bs-popper] {
right: 0;
left: auto;
}
}
}
}
// scss-docs-end responsive-breakpoints
// Allow for dropdowns to go bottom up (aka, dropup-menu)
// Just add .dropup after the standard .dropdown class and you're set.
.dropup {
.dropdown-menu[data-bs-popper] {
top: auto;
bottom: 100%;
margin-top: 0;
margin-bottom: var(--#{$prefix}dropdown-spacer);
}
.dropdown-toggle {
@include caret(up);
}
}
.dropend {
.dropdown-menu[data-bs-popper] {
top: 0;
right: auto;
left: 100%;
margin-top: 0;
margin-left: var(--#{$prefix}dropdown-spacer);
}
.dropdown-toggle {
@include caret(end);
&::after {
vertical-align: 0;
}
}
}
.dropstart {
.dropdown-menu[data-bs-popper] {
top: 0;
right: 100%;
left: auto;
margin-top: 0;
margin-right: var(--#{$prefix}dropdown-spacer);
}
.dropdown-toggle {
@include caret(start);
&::before {
vertical-align: 0;
}
}
}
// Dividers (basically an `<hr>`) within the dropdown
.dropdown-divider {
height: 0;
margin: var(--#{$prefix}dropdown-divider-margin-y) 0;
overflow: hidden;
border-top: 1px solid var(--#{$prefix}dropdown-divider-bg);
opacity: 1; // Revisit in v6 to de-dupe styles that conflict with <hr> element
}
// Links, buttons, and more within the dropdown menu
//
// `<button>`-specific styles are denoted with `// For <button>s`
.dropdown-item {
display: block;
width: 100%; // For `<button>`s
padding: var(--#{$prefix}dropdown-item-padding-y) var(--#{$prefix}dropdown-item-padding-x);
clear: both;
font-weight: $font-weight-normal;
color: var(--#{$prefix}dropdown-link-color);
text-align: inherit; // For `<button>`s
text-decoration: if($link-decoration == none, null, none);
white-space: nowrap; // prevent links from randomly breaking onto new lines
background-color: transparent; // For `<button>`s
border: 0; // For `<button>`s
@include border-radius(var(--#{$prefix}dropdown-item-border-radius, 0));
&:hover,
&:focus {
color: var(--#{$prefix}dropdown-link-hover-color);
text-decoration: if($link-hover-decoration == underline, none, null);
@include gradient-bg(var(--#{$prefix}dropdown-link-hover-bg));
}
&.active,
&:active {
color: var(--#{$prefix}dropdown-link-active-color);
text-decoration: none;
@include gradient-bg(var(--#{$prefix}dropdown-link-active-bg));
}
&.disabled,
&:disabled {
color: var(--#{$prefix}dropdown-link-disabled-color);
pointer-events: none;
background-color: transparent;
// Remove CSS gradients if they're enabled
background-image: if($enable-gradients, none, null);
}
}
.dropdown-menu.show {
display: block;
}
// Dropdown section headers
.dropdown-header {
display: block;
padding: var(--#{$prefix}dropdown-header-padding-y) var(--#{$prefix}dropdown-header-padding-x);
margin-bottom: 0; // for use with heading elements
@include font-size($font-size-sm);
color: var(--#{$prefix}dropdown-header-color);
white-space: nowrap; // as with > li > a
}
// Dropdown text
.dropdown-item-text {
display: block;
padding: var(--#{$prefix}dropdown-item-padding-y) var(--#{$prefix}dropdown-item-padding-x);
color: var(--#{$prefix}dropdown-link-color);
}
// Dark dropdowns
.dropdown-menu-dark {
// scss-docs-start dropdown-dark-css-vars
--#{$prefix}dropdown-color: #{$dropdown-dark-color};
--#{$prefix}dropdown-bg: #{$dropdown-dark-bg};
--#{$prefix}dropdown-border-color: #{$dropdown-dark-border-color};
--#{$prefix}dropdown-box-shadow: #{$dropdown-dark-box-shadow};
--#{$prefix}dropdown-link-color: #{$dropdown-dark-link-color};
--#{$prefix}dropdown-link-hover-color: #{$dropdown-dark-link-hover-color};
--#{$prefix}dropdown-divider-bg: #{$dropdown-dark-divider-bg};
--#{$prefix}dropdown-link-hover-bg: #{$dropdown-dark-link-hover-bg};
--#{$prefix}dropdown-link-active-color: #{$dropdown-dark-link-active-color};
--#{$prefix}dropdown-link-active-bg: #{$dropdown-dark-link-active-bg};
--#{$prefix}dropdown-link-disabled-color: #{$dropdown-dark-link-disabled-color};
--#{$prefix}dropdown-header-color: #{$dropdown-dark-header-color};
// scss-docs-end dropdown-dark-css-vars
}

View File

@@ -0,0 +1,9 @@
@import "forms/labels";
@import "forms/form-text";
@import "forms/form-control";
@import "forms/form-select";
@import "forms/form-check";
@import "forms/form-range";
@import "forms/floating-labels";
@import "forms/input-group";
@import "forms/validation";

View File

@@ -0,0 +1,302 @@
// Bootstrap functions
//
// Utility mixins and functions for evaluating source code across our variables, maps, and mixins.
// Ascending
// Used to evaluate Sass maps like our grid breakpoints.
@mixin _assert-ascending($map, $map-name) {
$prev-key: null;
$prev-num: null;
@each $key, $num in $map {
@if $prev-num == null or unit($num) == "%" or unit($prev-num) == "%" {
// Do nothing
} @else if not comparable($prev-num, $num) {
@warn "Potentially invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} whose unit makes it incomparable to #{$prev-num}, the value of the previous key '#{$prev-key}' !";
} @else if $prev-num >= $num {
@warn "Invalid value for #{$map-name}: This map must be in ascending order, but key '#{$key}' has value #{$num} which isn't greater than #{$prev-num}, the value of the previous key '#{$prev-key}' !";
}
$prev-key: $key;
$prev-num: $num;
}
}
// Starts at zero
// Used to ensure the min-width of the lowest breakpoint starts at 0.
@mixin _assert-starts-at-zero($map, $map-name: "$grid-breakpoints") {
@if length($map) > 0 {
$values: map-values($map);
$first-value: nth($values, 1);
@if $first-value != 0 {
@warn "First breakpoint in #{$map-name} must start at 0, but starts at #{$first-value}.";
}
}
}
// Colors
@function to-rgb($value) {
@return red($value), green($value), blue($value);
}
// stylelint-disable scss/dollar-variable-pattern
@function rgba-css-var($identifier, $target) {
@if $identifier == "body" and $target == "bg" {
@return rgba(var(--#{$prefix}#{$identifier}-bg-rgb), var(--#{$prefix}#{$target}-opacity));
} @if $identifier == "body" and $target == "text" {
@return rgba(var(--#{$prefix}#{$identifier}-color-rgb), var(--#{$prefix}#{$target}-opacity));
} @else {
@return rgba(var(--#{$prefix}#{$identifier}-rgb), var(--#{$prefix}#{$target}-opacity));
}
}
@function map-loop($map, $func, $args...) {
$_map: ();
@each $key, $value in $map {
// allow to pass the $key and $value of the map as an function argument
$_args: ();
@each $arg in $args {
$_args: append($_args, if($arg == "$key", $key, if($arg == "$value", $value, $arg)));
}
$_map: map-merge($_map, ($key: call(get-function($func), $_args...)));
}
@return $_map;
}
// stylelint-enable scss/dollar-variable-pattern
@function varify($list) {
$result: null;
@each $entry in $list {
$result: append($result, var(--#{$prefix}#{$entry}), space);
}
@return $result;
}
// Internal Bootstrap function to turn maps into its negative variant.
// It prefixes the keys with `n` and makes the value negative.
@function negativify-map($map) {
$result: ();
@each $key, $value in $map {
@if $key != 0 {
$result: map-merge($result, ("n" + $key: (-$value)));
}
}
@return $result;
}
// Get multiple keys from a sass map
@function map-get-multiple($map, $values) {
$result: ();
@each $key, $value in $map {
@if (index($values, $key) != null) {
$result: map-merge($result, ($key: $value));
}
}
@return $result;
}
// Merge multiple maps
@function map-merge-multiple($maps...) {
$merged-maps: ();
@each $map in $maps {
$merged-maps: map-merge($merged-maps, $map);
}
@return $merged-maps;
}
// Replace `$search` with `$replace` in `$string`
// Used on our SVG icon backgrounds for custom forms.
//
// @author Kitty Giraudel
// @param {String} $string - Initial string
// @param {String} $search - Substring to replace
// @param {String} $replace ('') - New value
// @return {String} - Updated string
@function str-replace($string, $search, $replace: "") {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
}
@return $string;
}
// See https://codepen.io/kevinweber/pen/dXWoRw
//
// Requires the use of quotes around data URIs.
@function escape-svg($string) {
@if str-index($string, "data:image/svg+xml") {
@each $char, $encoded in $escaped-characters {
// Do not escape the url brackets
@if str-index($string, "url(") == 1 {
$string: url("#{str-replace(str-slice($string, 6, -3), $char, $encoded)}");
} @else {
$string: str-replace($string, $char, $encoded);
}
}
}
@return $string;
}
// Color contrast
// See https://github.com/twbs/bootstrap/pull/30168
// A list of pre-calculated numbers of pow(divide((divide($value, 255) + .055), 1.055), 2.4). (from 0 to 255)
// stylelint-disable-next-line scss/dollar-variable-default, scss/dollar-variable-pattern
$_luminance-list: .0008 .001 .0011 .0013 .0015 .0017 .002 .0022 .0025 .0027 .003 .0033 .0037 .004 .0044 .0048 .0052 .0056 .006 .0065 .007 .0075 .008 .0086 .0091 .0097 .0103 .011 .0116 .0123 .013 .0137 .0144 .0152 .016 .0168 .0176 .0185 .0194 .0203 .0212 .0222 .0232 .0242 .0252 .0262 .0273 .0284 .0296 .0307 .0319 .0331 .0343 .0356 .0369 .0382 .0395 .0409 .0423 .0437 .0452 .0467 .0482 .0497 .0513 .0529 .0545 .0561 .0578 .0595 .0612 .063 .0648 .0666 .0685 .0704 .0723 .0742 .0762 .0782 .0802 .0823 .0844 .0865 .0887 .0908 .0931 .0953 .0976 .0999 .1022 .1046 .107 .1095 .1119 .1144 .117 .1195 .1221 .1248 .1274 .1301 .1329 .1356 .1384 .1413 .1441 .147 .15 .1529 .1559 .159 .162 .1651 .1683 .1714 .1746 .1779 .1812 .1845 .1878 .1912 .1946 .1981 .2016 .2051 .2086 .2122 .2159 .2195 .2232 .227 .2307 .2346 .2384 .2423 .2462 .2502 .2542 .2582 .2623 .2664 .2705 .2747 .2789 .2831 .2874 .2918 .2961 .3005 .305 .3095 .314 .3185 .3231 .3278 .3325 .3372 .3419 .3467 .3515 .3564 .3613 .3663 .3712 .3763 .3813 .3864 .3916 .3968 .402 .4072 .4125 .4179 .4233 .4287 .4342 .4397 .4452 .4508 .4564 .4621 .4678 .4735 .4793 .4851 .491 .4969 .5029 .5089 .5149 .521 .5271 .5333 .5395 .5457 .552 .5583 .5647 .5711 .5776 .5841 .5906 .5972 .6038 .6105 .6172 .624 .6308 .6376 .6445 .6514 .6584 .6654 .6724 .6795 .6867 .6939 .7011 .7084 .7157 .7231 .7305 .7379 .7454 .7529 .7605 .7682 .7758 .7835 .7913 .7991 .807 .8148 .8228 .8308 .8388 .8469 .855 .8632 .8714 .8796 .8879 .8963 .9047 .9131 .9216 .9301 .9387 .9473 .956 .9647 .9734 .9823 .9911 1;
@function color-contrast($background, $color-contrast-dark: $color-contrast-dark, $color-contrast-light: $color-contrast-light, $min-contrast-ratio: $min-contrast-ratio) {
$foregrounds: $color-contrast-light, $color-contrast-dark, $white, $black;
$max-ratio: 0;
$max-ratio-color: null;
@each $color in $foregrounds {
$contrast-ratio: contrast-ratio($background, $color);
@if $contrast-ratio >= $min-contrast-ratio {
@return $color;
} @else if $contrast-ratio > $max-ratio {
$max-ratio: $contrast-ratio;
$max-ratio-color: $color;
}
}
@warn "Found no color leading to #{$min-contrast-ratio}:1 contrast ratio against #{$background}...";
@return $max-ratio-color;
}
@function contrast-ratio($background, $foreground: $color-contrast-light) {
$l1: luminance($background);
$l2: luminance(opaque($background, $foreground));
@return if($l1 > $l2, divide($l1 + .05, $l2 + .05), divide($l2 + .05, $l1 + .05));
}
// Return WCAG2.2 relative luminance
// See https://www.w3.org/TR/WCAG/#dfn-relative-luminance
// See https://www.w3.org/TR/WCAG/#dfn-contrast-ratio
@function luminance($color) {
$rgb: (
"r": red($color),
"g": green($color),
"b": blue($color)
);
@each $name, $value in $rgb {
$value: if(divide($value, 255) < .04045, divide(divide($value, 255), 12.92), nth($_luminance-list, $value + 1));
$rgb: map-merge($rgb, ($name: $value));
}
@return (map-get($rgb, "r") * .2126) + (map-get($rgb, "g") * .7152) + (map-get($rgb, "b") * .0722);
}
// Return opaque color
// opaque(#fff, rgba(0, 0, 0, .5)) => #808080
@function opaque($background, $foreground) {
@return mix(rgba($foreground, 1), $background, opacity($foreground) * 100%);
}
// scss-docs-start color-functions
// Tint a color: mix a color with white
@function tint-color($color, $weight) {
@return mix(white, $color, $weight);
}
// Shade a color: mix a color with black
@function shade-color($color, $weight) {
@return mix(black, $color, $weight);
}
// Shade the color if the weight is positive, else tint it
@function shift-color($color, $weight) {
@return if($weight > 0, shade-color($color, $weight), tint-color($color, -$weight));
}
// scss-docs-end color-functions
// Return valid calc
@function add($value1, $value2, $return-calc: true) {
@if $value1 == null {
@return $value2;
}
@if $value2 == null {
@return $value1;
}
@if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) {
@return $value1 + $value2;
}
@return if($return-calc == true, calc(#{$value1} + #{$value2}), $value1 + unquote(" + ") + $value2);
}
@function subtract($value1, $value2, $return-calc: true) {
@if $value1 == null and $value2 == null {
@return null;
}
@if $value1 == null {
@return -$value2;
}
@if $value2 == null {
@return $value1;
}
@if type-of($value1) == number and type-of($value2) == number and comparable($value1, $value2) {
@return $value1 - $value2;
}
@if type-of($value2) != number {
$value2: unquote("(") + $value2 + unquote(")");
}
@return if($return-calc == true, calc(#{$value1} - #{$value2}), $value1 + unquote(" - ") + $value2);
}
@function divide($dividend, $divisor, $precision: 10) {
$sign: if($dividend > 0 and $divisor > 0 or $dividend < 0 and $divisor < 0, 1, -1);
$dividend: abs($dividend);
$divisor: abs($divisor);
@if $dividend == 0 {
@return 0;
}
@if $divisor == 0 {
@error "Cannot divide by 0";
}
$remainder: $dividend;
$result: 0;
$factor: 10;
@while ($remainder > 0 and $precision >= 0) {
$quotient: 0;
@while ($remainder >= $divisor) {
$remainder: $remainder - $divisor;
$quotient: $quotient + 1;
}
$result: $result * 10 + $quotient;
$factor: $factor * .1;
$remainder: $remainder * 10;
$precision: $precision - 1;
@if ($precision < 0 and $remainder >= $divisor * 5) {
$result: $result + 1;
}
}
$result: $result * $factor * $sign;
$dividend-unit: unit($dividend);
$divisor-unit: unit($divisor);
$unit-map: (
"px": 1px,
"rem": 1rem,
"em": 1em,
"%": 1%
);
@if ($dividend-unit != $divisor-unit and map-has-key($unit-map, $dividend-unit)) {
$result: $result * map-get($unit-map, $dividend-unit);
}
@return $result;
}

View File

@@ -0,0 +1,39 @@
// Row
//
// Rows contain your columns.
:root {
@each $name, $value in $grid-breakpoints {
--#{$prefix}breakpoint-#{$name}: #{$value};
}
}
@if $enable-grid-classes {
.row {
@include make-row();
> * {
@include make-col-ready();
}
}
}
@if $enable-cssgrid {
.grid {
display: grid;
grid-template-rows: repeat(var(--#{$prefix}rows, 1), 1fr);
grid-template-columns: repeat(var(--#{$prefix}columns, #{$grid-columns}), 1fr);
gap: var(--#{$prefix}gap, #{$grid-gutter-width});
@include make-cssgrid();
}
}
// Columns
//
// Common styles for small and large grid columns
@if $enable-grid-classes {
@include make-grid-columns();
}

View File

@@ -0,0 +1,12 @@
@import "helpers/clearfix";
@import "helpers/color-bg";
@import "helpers/colored-links";
@import "helpers/focus-ring";
@import "helpers/icon-link";
@import "helpers/ratio";
@import "helpers/position";
@import "helpers/stacks";
@import "helpers/visually-hidden";
@import "helpers/stretched-link";
@import "helpers/text-truncation";
@import "helpers/vr";

View File

@@ -0,0 +1,42 @@
// Responsive images (ensure images don't scale beyond their parents)
//
// This is purposefully opt-in via an explicit class rather than being the default for all `<img>`s.
// We previously tried the "images are responsive by default" approach in Bootstrap v2,
// and abandoned it in Bootstrap v3 because it breaks lots of third-party widgets (including Google Maps)
// which weren't expecting the images within themselves to be involuntarily resized.
// See also https://github.com/twbs/bootstrap/issues/18178
.img-fluid {
@include img-fluid();
}
// Image thumbnails
.img-thumbnail {
padding: $thumbnail-padding;
background-color: $thumbnail-bg;
border: $thumbnail-border-width solid $thumbnail-border-color;
@include border-radius($thumbnail-border-radius);
@include box-shadow($thumbnail-box-shadow);
// Keep them at most 100% wide
@include img-fluid();
}
//
// Figures
//
.figure {
// Ensures the caption's text aligns with the image.
display: inline-block;
}
.figure-img {
margin-bottom: $spacer * .5;
line-height: 1;
}
.figure-caption {
@include font-size($figure-caption-font-size);
color: $figure-caption-color;
}

View File

@@ -0,0 +1,199 @@
// Base class
//
// Easily usable on <ul>, <ol>, or <div>.
.list-group {
// scss-docs-start list-group-css-vars
--#{$prefix}list-group-color: #{$list-group-color};
--#{$prefix}list-group-bg: #{$list-group-bg};
--#{$prefix}list-group-border-color: #{$list-group-border-color};
--#{$prefix}list-group-border-width: #{$list-group-border-width};
--#{$prefix}list-group-border-radius: #{$list-group-border-radius};
--#{$prefix}list-group-item-padding-x: #{$list-group-item-padding-x};
--#{$prefix}list-group-item-padding-y: #{$list-group-item-padding-y};
--#{$prefix}list-group-action-color: #{$list-group-action-color};
--#{$prefix}list-group-action-hover-color: #{$list-group-action-hover-color};
--#{$prefix}list-group-action-hover-bg: #{$list-group-hover-bg};
--#{$prefix}list-group-action-active-color: #{$list-group-action-active-color};
--#{$prefix}list-group-action-active-bg: #{$list-group-action-active-bg};
--#{$prefix}list-group-disabled-color: #{$list-group-disabled-color};
--#{$prefix}list-group-disabled-bg: #{$list-group-disabled-bg};
--#{$prefix}list-group-active-color: #{$list-group-active-color};
--#{$prefix}list-group-active-bg: #{$list-group-active-bg};
--#{$prefix}list-group-active-border-color: #{$list-group-active-border-color};
// scss-docs-end list-group-css-vars
display: flex;
flex-direction: column;
// No need to set list-style: none; since .list-group-item is block level
padding-left: 0; // reset padding because ul and ol
margin-bottom: 0;
@include border-radius(var(--#{$prefix}list-group-border-radius));
}
.list-group-numbered {
list-style-type: none;
counter-reset: section;
> .list-group-item::before {
// Increments only this instance of the section counter
content: counters(section, ".") ". ";
counter-increment: section;
}
}
// Individual list items
//
// Use on `li`s or `div`s within the `.list-group` parent.
.list-group-item {
position: relative;
display: block;
padding: var(--#{$prefix}list-group-item-padding-y) var(--#{$prefix}list-group-item-padding-x);
color: var(--#{$prefix}list-group-color);
text-decoration: if($link-decoration == none, null, none);
background-color: var(--#{$prefix}list-group-bg);
border: var(--#{$prefix}list-group-border-width) solid var(--#{$prefix}list-group-border-color);
&:first-child {
@include border-top-radius(inherit);
}
&:last-child {
@include border-bottom-radius(inherit);
}
&.disabled,
&:disabled {
color: var(--#{$prefix}list-group-disabled-color);
pointer-events: none;
background-color: var(--#{$prefix}list-group-disabled-bg);
}
// Include both here for `<a>`s and `<button>`s
&.active {
z-index: 2; // Place active items above their siblings for proper border styling
color: var(--#{$prefix}list-group-active-color);
background-color: var(--#{$prefix}list-group-active-bg);
border-color: var(--#{$prefix}list-group-active-border-color);
}
// stylelint-disable-next-line scss/selector-no-redundant-nesting-selector
& + .list-group-item {
border-top-width: 0;
&.active {
margin-top: calc(-1 * var(--#{$prefix}list-group-border-width)); // stylelint-disable-line function-disallowed-list
border-top-width: var(--#{$prefix}list-group-border-width);
}
}
}
// Interactive list items
//
// Use anchor or button elements instead of `li`s or `div`s to create interactive
// list items. Includes an extra `.active` modifier class for selected items.
.list-group-item-action {
width: 100%; // For `<button>`s (anchors become 100% by default though)
color: var(--#{$prefix}list-group-action-color);
text-align: inherit; // For `<button>`s (anchors inherit)
&:not(.active) {
// Hover state
&:hover,
&:focus {
z-index: 1; // Place hover/focus items above their siblings for proper border styling
color: var(--#{$prefix}list-group-action-hover-color);
text-decoration: none;
background-color: var(--#{$prefix}list-group-action-hover-bg);
}
&:active {
color: var(--#{$prefix}list-group-action-active-color);
background-color: var(--#{$prefix}list-group-action-active-bg);
}
}
}
// Horizontal
//
// Change the layout of list group items from vertical (default) to horizontal.
@each $breakpoint in map-keys($grid-breakpoints) {
@include media-breakpoint-up($breakpoint) {
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
.list-group-horizontal#{$infix} {
flex-direction: row;
> .list-group-item {
&:first-child:not(:last-child) {
@include border-bottom-start-radius(var(--#{$prefix}list-group-border-radius));
@include border-top-end-radius(0);
}
&:last-child:not(:first-child) {
@include border-top-end-radius(var(--#{$prefix}list-group-border-radius));
@include border-bottom-start-radius(0);
}
&.active {
margin-top: 0;
}
+ .list-group-item {
border-top-width: var(--#{$prefix}list-group-border-width);
border-left-width: 0;
&.active {
margin-left: calc(-1 * var(--#{$prefix}list-group-border-width)); // stylelint-disable-line function-disallowed-list
border-left-width: var(--#{$prefix}list-group-border-width);
}
}
}
}
}
}
// Flush list items
//
// Remove borders and border-radius to keep list group items edge-to-edge. Most
// useful within other components (e.g., cards).
.list-group-flush {
@include border-radius(0);
> .list-group-item {
border-width: 0 0 var(--#{$prefix}list-group-border-width);
&:last-child {
border-bottom-width: 0;
}
}
}
// scss-docs-start list-group-modifiers
// List group contextual variants
//
// Add modifier classes to change text and background color on individual items.
// Organizationally, this must come after the `:hover` states.
@each $state in map-keys($theme-colors) {
.list-group-item-#{$state} {
--#{$prefix}list-group-color: var(--#{$prefix}#{$state}-text-emphasis);
--#{$prefix}list-group-bg: var(--#{$prefix}#{$state}-bg-subtle);
--#{$prefix}list-group-border-color: var(--#{$prefix}#{$state}-border-subtle);
--#{$prefix}list-group-action-hover-color: var(--#{$prefix}emphasis-color);
--#{$prefix}list-group-action-hover-bg: var(--#{$prefix}#{$state}-border-subtle);
--#{$prefix}list-group-action-active-color: var(--#{$prefix}emphasis-color);
--#{$prefix}list-group-action-active-bg: var(--#{$prefix}#{$state}-border-subtle);
--#{$prefix}list-group-active-color: var(--#{$prefix}#{$state}-bg-subtle);
--#{$prefix}list-group-active-bg: var(--#{$prefix}#{$state}-text-emphasis);
--#{$prefix}list-group-active-border-color: var(--#{$prefix}#{$state}-text-emphasis);
}
}
// scss-docs-end list-group-modifiers

View File

@@ -0,0 +1,174 @@
// Re-assigned maps
//
// Placed here so that others can override the default Sass maps and see automatic updates to utilities and more.
// scss-docs-start theme-colors-rgb
$theme-colors-rgb: map-loop($theme-colors, to-rgb, "$value") !default;
// scss-docs-end theme-colors-rgb
// scss-docs-start theme-text-map
$theme-colors-text: (
"primary": $primary-text-emphasis,
"secondary": $secondary-text-emphasis,
"success": $success-text-emphasis,
"info": $info-text-emphasis,
"warning": $warning-text-emphasis,
"danger": $danger-text-emphasis,
"light": $light-text-emphasis,
"dark": $dark-text-emphasis,
) !default;
// scss-docs-end theme-text-map
// scss-docs-start theme-bg-subtle-map
$theme-colors-bg-subtle: (
"primary": $primary-bg-subtle,
"secondary": $secondary-bg-subtle,
"success": $success-bg-subtle,
"info": $info-bg-subtle,
"warning": $warning-bg-subtle,
"danger": $danger-bg-subtle,
"light": $light-bg-subtle,
"dark": $dark-bg-subtle,
) !default;
// scss-docs-end theme-bg-subtle-map
// scss-docs-start theme-border-subtle-map
$theme-colors-border-subtle: (
"primary": $primary-border-subtle,
"secondary": $secondary-border-subtle,
"success": $success-border-subtle,
"info": $info-border-subtle,
"warning": $warning-border-subtle,
"danger": $danger-border-subtle,
"light": $light-border-subtle,
"dark": $dark-border-subtle,
) !default;
// scss-docs-end theme-border-subtle-map
$theme-colors-text-dark: null !default;
$theme-colors-bg-subtle-dark: null !default;
$theme-colors-border-subtle-dark: null !default;
@if $enable-dark-mode {
// scss-docs-start theme-text-dark-map
$theme-colors-text-dark: (
"primary": $primary-text-emphasis-dark,
"secondary": $secondary-text-emphasis-dark,
"success": $success-text-emphasis-dark,
"info": $info-text-emphasis-dark,
"warning": $warning-text-emphasis-dark,
"danger": $danger-text-emphasis-dark,
"light": $light-text-emphasis-dark,
"dark": $dark-text-emphasis-dark,
) !default;
// scss-docs-end theme-text-dark-map
// scss-docs-start theme-bg-subtle-dark-map
$theme-colors-bg-subtle-dark: (
"primary": $primary-bg-subtle-dark,
"secondary": $secondary-bg-subtle-dark,
"success": $success-bg-subtle-dark,
"info": $info-bg-subtle-dark,
"warning": $warning-bg-subtle-dark,
"danger": $danger-bg-subtle-dark,
"light": $light-bg-subtle-dark,
"dark": $dark-bg-subtle-dark,
) !default;
// scss-docs-end theme-bg-subtle-dark-map
// scss-docs-start theme-border-subtle-dark-map
$theme-colors-border-subtle-dark: (
"primary": $primary-border-subtle-dark,
"secondary": $secondary-border-subtle-dark,
"success": $success-border-subtle-dark,
"info": $info-border-subtle-dark,
"warning": $warning-border-subtle-dark,
"danger": $danger-border-subtle-dark,
"light": $light-border-subtle-dark,
"dark": $dark-border-subtle-dark,
) !default;
// scss-docs-end theme-border-subtle-dark-map
}
// Utilities maps
//
// Extends the default `$theme-colors` maps to help create our utilities.
// Come v6, we'll de-dupe these variables. Until then, for backward compatibility, we keep them to reassign.
// scss-docs-start utilities-colors
$utilities-colors: $theme-colors-rgb !default;
// scss-docs-end utilities-colors
// scss-docs-start utilities-text-colors
$utilities-text: map-merge(
$utilities-colors,
(
"black": to-rgb($black),
"white": to-rgb($white),
"body": to-rgb($body-color)
)
) !default;
$utilities-text-colors: map-loop($utilities-text, rgba-css-var, "$key", "text") !default;
$utilities-text-emphasis-colors: (
"primary-emphasis": var(--#{$prefix}primary-text-emphasis),
"secondary-emphasis": var(--#{$prefix}secondary-text-emphasis),
"success-emphasis": var(--#{$prefix}success-text-emphasis),
"info-emphasis": var(--#{$prefix}info-text-emphasis),
"warning-emphasis": var(--#{$prefix}warning-text-emphasis),
"danger-emphasis": var(--#{$prefix}danger-text-emphasis),
"light-emphasis": var(--#{$prefix}light-text-emphasis),
"dark-emphasis": var(--#{$prefix}dark-text-emphasis)
) !default;
// scss-docs-end utilities-text-colors
// scss-docs-start utilities-bg-colors
$utilities-bg: map-merge(
$utilities-colors,
(
"black": to-rgb($black),
"white": to-rgb($white),
"body": to-rgb($body-bg)
)
) !default;
$utilities-bg-colors: map-loop($utilities-bg, rgba-css-var, "$key", "bg") !default;
$utilities-bg-subtle: (
"primary-subtle": var(--#{$prefix}primary-bg-subtle),
"secondary-subtle": var(--#{$prefix}secondary-bg-subtle),
"success-subtle": var(--#{$prefix}success-bg-subtle),
"info-subtle": var(--#{$prefix}info-bg-subtle),
"warning-subtle": var(--#{$prefix}warning-bg-subtle),
"danger-subtle": var(--#{$prefix}danger-bg-subtle),
"light-subtle": var(--#{$prefix}light-bg-subtle),
"dark-subtle": var(--#{$prefix}dark-bg-subtle)
) !default;
// scss-docs-end utilities-bg-colors
// scss-docs-start utilities-border-colors
$utilities-border: map-merge(
$utilities-colors,
(
"black": to-rgb($black),
"white": to-rgb($white)
)
) !default;
$utilities-border-colors: map-loop($utilities-border, rgba-css-var, "$key", "border") !default;
$utilities-border-subtle: (
"primary-subtle": var(--#{$prefix}primary-border-subtle),
"secondary-subtle": var(--#{$prefix}secondary-border-subtle),
"success-subtle": var(--#{$prefix}success-border-subtle),
"info-subtle": var(--#{$prefix}info-border-subtle),
"warning-subtle": var(--#{$prefix}warning-border-subtle),
"danger-subtle": var(--#{$prefix}danger-border-subtle),
"light-subtle": var(--#{$prefix}light-border-subtle),
"dark-subtle": var(--#{$prefix}dark-border-subtle)
) !default;
// scss-docs-end utilities-border-colors
$utilities-links-underline: map-loop($utilities-colors, rgba-css-var, "$key", "link-underline") !default;
$negative-spacers: if($enable-negative-margins, negativify-map($spacers), null) !default;
$gutters: $spacers !default;

View File

@@ -0,0 +1,42 @@
// Toggles
//
// Used in conjunction with global variables to enable certain theme features.
// Vendor
@import "vendor/rfs";
// Deprecate
@import "mixins/deprecate";
// Helpers
@import "mixins/breakpoints";
@import "mixins/color-mode";
@import "mixins/color-scheme";
@import "mixins/image";
@import "mixins/resize";
@import "mixins/visually-hidden";
@import "mixins/reset-text";
@import "mixins/text-truncate";
// Utilities
@import "mixins/utilities";
// Components
@import "mixins/backdrop";
@import "mixins/buttons";
@import "mixins/caret";
@import "mixins/pagination";
@import "mixins/lists";
@import "mixins/forms";
@import "mixins/table-variants";
// Skins
@import "mixins/border-radius";
@import "mixins/box-shadow";
@import "mixins/gradients";
@import "mixins/transition";
// Layout
@import "mixins/clearfix";
@import "mixins/container";
@import "mixins/grid";

View File

@@ -0,0 +1,240 @@
// stylelint-disable function-disallowed-list
// .modal-open - body class for killing the scroll
// .modal - container to scroll within
// .modal-dialog - positioning shell for the actual modal
// .modal-content - actual modal w/ bg and corners and stuff
// Container that the modal scrolls within
.modal {
// scss-docs-start modal-css-vars
--#{$prefix}modal-zindex: #{$zindex-modal};
--#{$prefix}modal-width: #{$modal-md};
--#{$prefix}modal-padding: #{$modal-inner-padding};
--#{$prefix}modal-margin: #{$modal-dialog-margin};
--#{$prefix}modal-color: #{$modal-content-color};
--#{$prefix}modal-bg: #{$modal-content-bg};
--#{$prefix}modal-border-color: #{$modal-content-border-color};
--#{$prefix}modal-border-width: #{$modal-content-border-width};
--#{$prefix}modal-border-radius: #{$modal-content-border-radius};
--#{$prefix}modal-box-shadow: #{$modal-content-box-shadow-xs};
--#{$prefix}modal-inner-border-radius: #{$modal-content-inner-border-radius};
--#{$prefix}modal-header-padding-x: #{$modal-header-padding-x};
--#{$prefix}modal-header-padding-y: #{$modal-header-padding-y};
--#{$prefix}modal-header-padding: #{$modal-header-padding}; // Todo in v6: Split this padding into x and y
--#{$prefix}modal-header-border-color: #{$modal-header-border-color};
--#{$prefix}modal-header-border-width: #{$modal-header-border-width};
--#{$prefix}modal-title-line-height: #{$modal-title-line-height};
--#{$prefix}modal-footer-gap: #{$modal-footer-margin-between};
--#{$prefix}modal-footer-bg: #{$modal-footer-bg};
--#{$prefix}modal-footer-border-color: #{$modal-footer-border-color};
--#{$prefix}modal-footer-border-width: #{$modal-footer-border-width};
// scss-docs-end modal-css-vars
position: fixed;
top: 0;
left: 0;
z-index: var(--#{$prefix}modal-zindex);
display: none;
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
// Prevent Chrome on Windows from adding a focus outline. For details, see
// https://github.com/twbs/bootstrap/pull/10951.
outline: 0;
// We deliberately don't use `-webkit-overflow-scrolling: touch;` due to a
// gnarly iOS Safari bug: https://bugs.webkit.org/show_bug.cgi?id=158342
// See also https://github.com/twbs/bootstrap/issues/17695
}
// Shell div to position the modal with bottom padding
.modal-dialog {
position: relative;
width: auto;
margin: var(--#{$prefix}modal-margin);
// allow clicks to pass through for custom click handling to close modal
pointer-events: none;
// When fading in the modal, animate it to slide down
.modal.fade & {
transform: $modal-fade-transform;
@include transition($modal-transition);
}
.modal.show & {
transform: $modal-show-transform;
}
// When trying to close, animate focus to scale
.modal.modal-static & {
transform: $modal-scale-transform;
}
}
.modal-dialog-scrollable {
height: calc(100% - var(--#{$prefix}modal-margin) * 2);
.modal-content {
max-height: 100%;
overflow: hidden;
}
.modal-body {
overflow-y: auto;
}
}
.modal-dialog-centered {
display: flex;
align-items: center;
min-height: calc(100% - var(--#{$prefix}modal-margin) * 2);
}
// Actual modal
.modal-content {
position: relative;
display: flex;
flex-direction: column;
width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog`
// counteract the pointer-events: none; in the .modal-dialog
color: var(--#{$prefix}modal-color);
pointer-events: auto;
background-color: var(--#{$prefix}modal-bg);
background-clip: padding-box;
border: var(--#{$prefix}modal-border-width) solid var(--#{$prefix}modal-border-color);
@include border-radius(var(--#{$prefix}modal-border-radius));
@include box-shadow(var(--#{$prefix}modal-box-shadow));
// Remove focus outline from opened modal
outline: 0;
}
// Modal background
.modal-backdrop {
// scss-docs-start modal-backdrop-css-vars
--#{$prefix}backdrop-zindex: #{$zindex-modal-backdrop};
--#{$prefix}backdrop-bg: #{$modal-backdrop-bg};
--#{$prefix}backdrop-opacity: #{$modal-backdrop-opacity};
// scss-docs-end modal-backdrop-css-vars
@include overlay-backdrop(var(--#{$prefix}backdrop-zindex), var(--#{$prefix}backdrop-bg), var(--#{$prefix}backdrop-opacity));
}
// Modal header
// Top section of the modal w/ title and dismiss
.modal-header {
display: flex;
flex-shrink: 0;
align-items: center;
padding: var(--#{$prefix}modal-header-padding);
border-bottom: var(--#{$prefix}modal-header-border-width) solid var(--#{$prefix}modal-header-border-color);
@include border-top-radius(var(--#{$prefix}modal-inner-border-radius));
.btn-close {
padding: calc(var(--#{$prefix}modal-header-padding-y) * .5) calc(var(--#{$prefix}modal-header-padding-x) * .5);
// Split properties to avoid invalid calc() function if value is 0
margin-top: calc(-.5 * var(--#{$prefix}modal-header-padding-y));
margin-right: calc(-.5 * var(--#{$prefix}modal-header-padding-x));
margin-bottom: calc(-.5 * var(--#{$prefix}modal-header-padding-y));
margin-left: auto;
}
}
// Title text within header
.modal-title {
margin-bottom: 0;
line-height: var(--#{$prefix}modal-title-line-height);
}
// Modal body
// Where all modal content resides (sibling of .modal-header and .modal-footer)
.modal-body {
position: relative;
// Enable `flex-grow: 1` so that the body take up as much space as possible
// when there should be a fixed height on `.modal-dialog`.
flex: 1 1 auto;
padding: var(--#{$prefix}modal-padding);
}
// Footer (for actions)
.modal-footer {
display: flex;
flex-shrink: 0;
flex-wrap: wrap;
align-items: center; // vertically center
justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items
padding: calc(var(--#{$prefix}modal-padding) - var(--#{$prefix}modal-footer-gap) * .5);
background-color: var(--#{$prefix}modal-footer-bg);
border-top: var(--#{$prefix}modal-footer-border-width) solid var(--#{$prefix}modal-footer-border-color);
@include border-bottom-radius(var(--#{$prefix}modal-inner-border-radius));
// Place margin between footer elements
// This solution is far from ideal because of the universal selector usage,
// but is needed to fix https://github.com/twbs/bootstrap/issues/24800
> * {
margin: calc(var(--#{$prefix}modal-footer-gap) * .5); // Todo in v6: replace with gap on parent class
}
}
// Scale up the modal
@include media-breakpoint-up(sm) {
.modal {
--#{$prefix}modal-margin: #{$modal-dialog-margin-y-sm-up};
--#{$prefix}modal-box-shadow: #{$modal-content-box-shadow-sm-up};
}
// Automatically set modal's width for larger viewports
.modal-dialog {
max-width: var(--#{$prefix}modal-width);
margin-right: auto;
margin-left: auto;
}
.modal-sm {
--#{$prefix}modal-width: #{$modal-sm};
}
}
@include media-breakpoint-up(lg) {
.modal-lg,
.modal-xl {
--#{$prefix}modal-width: #{$modal-lg};
}
}
@include media-breakpoint-up(xl) {
.modal-xl {
--#{$prefix}modal-width: #{$modal-xl};
}
}
// scss-docs-start modal-fullscreen-loop
@each $breakpoint in map-keys($grid-breakpoints) {
$infix: breakpoint-infix($breakpoint, $grid-breakpoints);
$postfix: if($infix != "", $infix + "-down", "");
@include media-breakpoint-down($breakpoint) {
.modal-fullscreen#{$postfix} {
width: 100vw;
max-width: none;
height: 100%;
margin: 0;
.modal-content {
height: 100%;
border: 0;
@include border-radius(0);
}
.modal-header,
.modal-footer {
@include border-radius(0);
}
.modal-body {
overflow-y: auto;
}
}
}
}
// scss-docs-end modal-fullscreen-loop

View File

@@ -0,0 +1,197 @@
// Base class
//
// Kickstart any navigation component with a set of style resets. Works with
// `<nav>`s, `<ul>`s or `<ol>`s.
.nav {
// scss-docs-start nav-css-vars
--#{$prefix}nav-link-padding-x: #{$nav-link-padding-x};
--#{$prefix}nav-link-padding-y: #{$nav-link-padding-y};
@include rfs($nav-link-font-size, --#{$prefix}nav-link-font-size);
--#{$prefix}nav-link-font-weight: #{$nav-link-font-weight};
--#{$prefix}nav-link-color: #{$nav-link-color};
--#{$prefix}nav-link-hover-color: #{$nav-link-hover-color};
--#{$prefix}nav-link-disabled-color: #{$nav-link-disabled-color};
// scss-docs-end nav-css-vars
display: flex;
flex-wrap: wrap;
padding-left: 0;
margin-bottom: 0;
list-style: none;
}
.nav-link {
display: block;
padding: var(--#{$prefix}nav-link-padding-y) var(--#{$prefix}nav-link-padding-x);
@include font-size(var(--#{$prefix}nav-link-font-size));
font-weight: var(--#{$prefix}nav-link-font-weight);
color: var(--#{$prefix}nav-link-color);
text-decoration: if($link-decoration == none, null, none);
background: none;
border: 0;
@include transition($nav-link-transition);
&:hover,
&:focus {
color: var(--#{$prefix}nav-link-hover-color);
text-decoration: if($link-hover-decoration == underline, none, null);
}
&:focus-visible {
outline: 0;
box-shadow: $nav-link-focus-box-shadow;
}
// Disabled state lightens text
&.disabled,
&:disabled {
color: var(--#{$prefix}nav-link-disabled-color);
pointer-events: none;
cursor: default;
}
}
//
// Tabs
//
.nav-tabs {
// scss-docs-start nav-tabs-css-vars
--#{$prefix}nav-tabs-border-width: #{$nav-tabs-border-width};
--#{$prefix}nav-tabs-border-color: #{$nav-tabs-border-color};
--#{$prefix}nav-tabs-border-radius: #{$nav-tabs-border-radius};
--#{$prefix}nav-tabs-link-hover-border-color: #{$nav-tabs-link-hover-border-color};
--#{$prefix}nav-tabs-link-active-color: #{$nav-tabs-link-active-color};
--#{$prefix}nav-tabs-link-active-bg: #{$nav-tabs-link-active-bg};
--#{$prefix}nav-tabs-link-active-border-color: #{$nav-tabs-link-active-border-color};
// scss-docs-end nav-tabs-css-vars
border-bottom: var(--#{$prefix}nav-tabs-border-width) solid var(--#{$prefix}nav-tabs-border-color);
.nav-link {
margin-bottom: calc(-1 * var(--#{$prefix}nav-tabs-border-width)); // stylelint-disable-line function-disallowed-list
border: var(--#{$prefix}nav-tabs-border-width) solid transparent;
@include border-top-radius(var(--#{$prefix}nav-tabs-border-radius));
&:hover,
&:focus {
// Prevents active .nav-link tab overlapping focus outline of previous/next .nav-link
isolation: isolate;
border-color: var(--#{$prefix}nav-tabs-link-hover-border-color);
}
}
.nav-link.active,
.nav-item.show .nav-link {
color: var(--#{$prefix}nav-tabs-link-active-color);
background-color: var(--#{$prefix}nav-tabs-link-active-bg);
border-color: var(--#{$prefix}nav-tabs-link-active-border-color);
}
.dropdown-menu {
// Make dropdown border overlap tab border
margin-top: calc(-1 * var(--#{$prefix}nav-tabs-border-width)); // stylelint-disable-line function-disallowed-list
// Remove the top rounded corners here since there is a hard edge above the menu
@include border-top-radius(0);
}
}
//
// Pills
//
.nav-pills {
// scss-docs-start nav-pills-css-vars
--#{$prefix}nav-pills-border-radius: #{$nav-pills-border-radius};
--#{$prefix}nav-pills-link-active-color: #{$nav-pills-link-active-color};
--#{$prefix}nav-pills-link-active-bg: #{$nav-pills-link-active-bg};
// scss-docs-end nav-pills-css-vars
.nav-link {
@include border-radius(var(--#{$prefix}nav-pills-border-radius));
}
.nav-link.active,
.show > .nav-link {
color: var(--#{$prefix}nav-pills-link-active-color);
@include gradient-bg(var(--#{$prefix}nav-pills-link-active-bg));
}
}
//
// Underline
//
.nav-underline {
// scss-docs-start nav-underline-css-vars
--#{$prefix}nav-underline-gap: #{$nav-underline-gap};
--#{$prefix}nav-underline-border-width: #{$nav-underline-border-width};
--#{$prefix}nav-underline-link-active-color: #{$nav-underline-link-active-color};
// scss-docs-end nav-underline-css-vars
gap: var(--#{$prefix}nav-underline-gap);
.nav-link {
padding-right: 0;
padding-left: 0;
border-bottom: var(--#{$prefix}nav-underline-border-width) solid transparent;
&:hover,
&:focus {
border-bottom-color: currentcolor;
}
}
.nav-link.active,
.show > .nav-link {
font-weight: $font-weight-bold;
color: var(--#{$prefix}nav-underline-link-active-color);
border-bottom-color: currentcolor;
}
}
//
// Justified variants
//
.nav-fill {
> .nav-link,
.nav-item {
flex: 1 1 auto;
text-align: center;
}
}
.nav-justified {
> .nav-link,
.nav-item {
flex-grow: 1;
flex-basis: 0;
text-align: center;
}
}
.nav-fill,
.nav-justified {
.nav-item .nav-link {
width: 100%; // Make sure button will grow
}
}
// Tabbable tabs
//
// Hide tabbable panes to start, show them when `.active`
.tab-content {
> .tab-pane {
display: none;
}
> .active {
display: block;
}
}

Some files were not shown because too many files have changed in this diff Show More