feat: add TypeScript lessons and learning panel
- Introduced a new script to check TypeScript lesson files for errors. - Created a main TypeScript file to render lessons and their details. - Added lesson definitions with starter and answer codes. - Implemented a user interface for navigating and running lessons. - Styled the application with CSS for a better user experience. - Updated README to reflect the new TypeScript section and usage instructions.
This commit is contained in:
308
06-typescript/src/lessons.ts
Normal file
308
06-typescript/src/lessons.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import lesson01Starter from "../01-js-vs-ts/starter.ts?raw";
|
||||
import lesson01Answer from "../01-js-vs-ts/answer.ts?raw";
|
||||
import lesson02Starter from "../02-basic-type-annotations/starter.ts?raw";
|
||||
import lesson02Answer from "../02-basic-type-annotations/answer.ts?raw";
|
||||
import lesson03Starter from "../03-array-and-function-types/starter.ts?raw";
|
||||
import lesson03Answer from "../03-array-and-function-types/answer.ts?raw";
|
||||
import lesson04Starter from "../04-interface-object-shape/starter.ts?raw";
|
||||
import lesson04Answer from "../04-interface-object-shape/answer.ts?raw";
|
||||
import lesson05Starter from "../05-generic-functions/starter.ts?raw";
|
||||
import lesson05Answer from "../05-generic-functions/answer.ts?raw";
|
||||
import lesson06Starter from "../06-union-and-optional-props/starter.ts?raw";
|
||||
import lesson06Answer from "../06-union-and-optional-props/answer.ts?raw";
|
||||
import lesson07Starter from "../07-type-safe-renderer/starter.ts?raw";
|
||||
import lesson07Answer from "../07-type-safe-renderer/answer.ts?raw";
|
||||
import lesson08Starter from "../08-final-mini-app/starter.ts?raw";
|
||||
import lesson08Answer from "../08-final-mini-app/answer.ts?raw";
|
||||
|
||||
export interface Lesson {
|
||||
id: string;
|
||||
title: string;
|
||||
focus: string;
|
||||
summary: string;
|
||||
starterCode: string;
|
||||
answerCode: string;
|
||||
keyPoints: string[];
|
||||
runDemo: () => string[];
|
||||
}
|
||||
|
||||
function runLesson01(): string[] {
|
||||
function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
const result = add(1, 2);
|
||||
|
||||
return [
|
||||
`add(1, 2) => ${result}`,
|
||||
'如果写成 add(1, "2"),TypeScript 会在编码阶段报错。',
|
||||
];
|
||||
}
|
||||
|
||||
function runLesson02(): string[] {
|
||||
const age: number = 25;
|
||||
const userName: string = "李青";
|
||||
const isLearning: boolean = true;
|
||||
|
||||
return [
|
||||
`age 的类型是 number,值为 ${age}`,
|
||||
`userName 的类型是 string,值为 ${userName}`,
|
||||
`isLearning 的类型是 boolean,值为 ${isLearning}`,
|
||||
];
|
||||
}
|
||||
|
||||
function runLesson03(): string[] {
|
||||
const scoreList: number[] = [82, 90, 95];
|
||||
|
||||
function sum(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
const total = sum(88, 12);
|
||||
|
||||
return [
|
||||
`scoreList => [${scoreList.join(", ")}]`,
|
||||
`sum(88, 12) => ${total}`,
|
||||
];
|
||||
}
|
||||
|
||||
function runLesson04(): string[] {
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
const user: User = {
|
||||
id: 1,
|
||||
name: "小王",
|
||||
age: 22,
|
||||
};
|
||||
|
||||
return [
|
||||
`User.name => ${user.name}`,
|
||||
`User.age => ${user.age}`,
|
||||
"interface 让对象结构在写代码时就固定下来。",
|
||||
];
|
||||
}
|
||||
|
||||
function runLesson05(): string[] {
|
||||
function getData<T>(data: T): T {
|
||||
return data;
|
||||
}
|
||||
|
||||
const text = getData("hello");
|
||||
const count = getData(100);
|
||||
|
||||
return [
|
||||
`getData("hello") => ${text}`,
|
||||
`getData(100) => ${count}`,
|
||||
"泛型可以在复用函数时保留输入和输出的类型关系。",
|
||||
];
|
||||
}
|
||||
|
||||
function runLesson06(): string[] {
|
||||
type UserId = number | string;
|
||||
|
||||
interface Profile {
|
||||
id: UserId;
|
||||
name: string;
|
||||
age?: number;
|
||||
}
|
||||
|
||||
const profileA: Profile = {
|
||||
id: 1,
|
||||
name: "周周",
|
||||
};
|
||||
|
||||
const profileB: Profile = {
|
||||
id: "u-2",
|
||||
name: "小林",
|
||||
age: 24,
|
||||
};
|
||||
|
||||
return [
|
||||
`profileA.id => ${profileA.id}`,
|
||||
`profileB.id => ${profileB.id}`,
|
||||
`profileB.age => ${profileB.age}`,
|
||||
];
|
||||
}
|
||||
|
||||
function runLesson07(): string[] {
|
||||
interface Course {
|
||||
title: string;
|
||||
lessons: number;
|
||||
finished: boolean;
|
||||
}
|
||||
|
||||
const courses: Course[] = [
|
||||
{ title: "TypeScript 基础", lessons: 8, finished: true },
|
||||
{ title: "接口和泛型", lessons: 6, finished: false },
|
||||
];
|
||||
|
||||
function renderCourseLines(list: Course[]): string[] {
|
||||
return list.map((course) => {
|
||||
const status = course.finished ? "已完成" : "学习中";
|
||||
return `${course.title} - ${course.lessons} 节 - ${status}`;
|
||||
});
|
||||
}
|
||||
|
||||
return renderCourseLines(courses);
|
||||
}
|
||||
|
||||
function runLesson08(): string[] {
|
||||
type StudentId = number | string;
|
||||
|
||||
interface Course {
|
||||
title: string;
|
||||
finished: boolean;
|
||||
}
|
||||
|
||||
interface Student {
|
||||
id: StudentId;
|
||||
name: string;
|
||||
age?: number;
|
||||
courses: Course[];
|
||||
}
|
||||
|
||||
function pickFirst<T>(list: T[]): T {
|
||||
return list[0];
|
||||
}
|
||||
|
||||
function formatStudent(student: Student): string {
|
||||
const courseCount = student.courses.length;
|
||||
return `${student.name} 当前有 ${courseCount} 门课程,编号是 ${student.id}`;
|
||||
}
|
||||
|
||||
const student: Student = {
|
||||
id: "stu-1",
|
||||
name: "林晨",
|
||||
courses: [
|
||||
{ title: "基本类型", finished: true },
|
||||
{ title: "接口", finished: false },
|
||||
],
|
||||
};
|
||||
|
||||
const firstCourse = pickFirst(student.courses);
|
||||
|
||||
return [
|
||||
formatStudent(student),
|
||||
`第一门课程:${firstCourse.title}`,
|
||||
`完成状态:${firstCourse.finished ? "已完成" : "学习中"}`,
|
||||
];
|
||||
}
|
||||
|
||||
export const lessons: Lesson[] = [
|
||||
{
|
||||
id: "01",
|
||||
title: "JavaScript vs TypeScript",
|
||||
focus: "理解为什么要引入类型系统",
|
||||
summary: "从一个最简单的加法函数开始,看 TypeScript 如何把错误提前到编码阶段。",
|
||||
starterCode: lesson01Starter,
|
||||
answerCode: lesson01Answer,
|
||||
keyPoints: [
|
||||
"TypeScript 在写代码阶段检查参数类型",
|
||||
"运行前发现问题,比运行后排查问题更省成本",
|
||||
"最常见的第一步就是给函数参数和返回值加类型",
|
||||
],
|
||||
runDemo: runLesson01,
|
||||
},
|
||||
{
|
||||
id: "02",
|
||||
title: "基本类型标注",
|
||||
focus: "number / string / boolean",
|
||||
summary: "把最常见的值类型写清楚,建立最基础的类型意识。",
|
||||
starterCode: lesson02Starter,
|
||||
answerCode: lesson02Answer,
|
||||
keyPoints: [
|
||||
"基础值先学会标注类型",
|
||||
"看到报错时先分清期望类型和实际类型",
|
||||
"能写对基本类型后再往对象和函数走",
|
||||
],
|
||||
runDemo: runLesson02,
|
||||
},
|
||||
{
|
||||
id: "03",
|
||||
title: "数组和函数类型",
|
||||
focus: "number[] 与函数参数/返回值",
|
||||
summary: "把类型从单个值扩展到数组和函数,是进入业务代码前必须过的一关。",
|
||||
starterCode: lesson03Starter,
|
||||
answerCode: lesson03Answer,
|
||||
keyPoints: [
|
||||
"数组里的元素类型要保持一致",
|
||||
"函数参数和返回值都应该尽量明确",
|
||||
"函数签名是 TypeScript 最高频的使用场景之一",
|
||||
],
|
||||
runDemo: runLesson03,
|
||||
},
|
||||
{
|
||||
id: "04",
|
||||
title: "interface 对象结构",
|
||||
focus: "用接口约束对象字段",
|
||||
summary: "当数据结构开始变复杂时,先定义 shape,再写数据和逻辑。",
|
||||
starterCode: lesson04Starter,
|
||||
answerCode: lesson04Answer,
|
||||
keyPoints: [
|
||||
"对象字段越多,越应该先定义 interface",
|
||||
"接口让团队协作时更容易知道一个对象该长什么样",
|
||||
"字段缺失、字段类型不匹配都会更早暴露",
|
||||
],
|
||||
runDemo: runLesson04,
|
||||
},
|
||||
{
|
||||
id: "05",
|
||||
title: "泛型函数",
|
||||
focus: "保持输入输出的类型关系",
|
||||
summary: "泛型的重点不是复杂,而是让复用函数在不同类型下依然安全。",
|
||||
starterCode: lesson05Starter,
|
||||
answerCode: lesson05Answer,
|
||||
keyPoints: [
|
||||
"泛型适合处理同类输入输出关系",
|
||||
"输入是什么类型,输出就跟着保持什么类型",
|
||||
"先理解例子,再去记住 <T> 这个写法",
|
||||
],
|
||||
runDemo: runLesson05,
|
||||
},
|
||||
{
|
||||
id: "06",
|
||||
title: "联合类型和可选属性",
|
||||
focus: "number | string 与 age?",
|
||||
summary: "业务数据并不总是完全整齐,联合类型和可选属性就是为这种情况准备的。",
|
||||
starterCode: lesson06Starter,
|
||||
answerCode: lesson06Answer,
|
||||
keyPoints: [
|
||||
"联合类型描述多个合法输入分支",
|
||||
"可选属性适合那些可能缺失的字段",
|
||||
"真实业务里常常需要同时使用这两类能力",
|
||||
],
|
||||
runDemo: runLesson06,
|
||||
},
|
||||
{
|
||||
id: "07",
|
||||
title: "类型安全渲染",
|
||||
focus: "把接口用到列表渲染里",
|
||||
summary: "进入接近真实业务的场景,用类型约束一组列表数据和渲染函数。",
|
||||
starterCode: lesson07Starter,
|
||||
answerCode: lesson07Answer,
|
||||
keyPoints: [
|
||||
"先定义 Course,再定义 Course[]",
|
||||
"渲染函数应该接收明确的数据结构",
|
||||
"数据结构一旦变化,相关代码会一起被提醒",
|
||||
],
|
||||
runDemo: runLesson07,
|
||||
},
|
||||
{
|
||||
id: "08",
|
||||
title: "综合小练习",
|
||||
focus: "把常见类型能力组合起来",
|
||||
summary: "把联合类型、接口、可选属性和泛型组合起来,做一个完整的小示例。",
|
||||
starterCode: lesson08Starter,
|
||||
answerCode: lesson08Answer,
|
||||
keyPoints: [
|
||||
"综合练习的重点是把前面知识点串起来",
|
||||
"能读懂数据模型,通常就能更稳地写业务代码",
|
||||
"类型不是目的,减少错误和提升可维护性才是目的",
|
||||
],
|
||||
runDemo: runLesson08,
|
||||
},
|
||||
];
|
||||
151
06-typescript/src/main.ts
Normal file
151
06-typescript/src/main.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import "./style.css";
|
||||
import { lessons } from "./lessons";
|
||||
|
||||
function getRequiredElement<T extends Element>(
|
||||
selector: string,
|
||||
parent: ParentNode = document,
|
||||
): T {
|
||||
const element = parent.querySelector<T>(selector);
|
||||
|
||||
if (!element) {
|
||||
throw new Error(`Required element was not found: ${selector}`);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
const app = getRequiredElement<HTMLDivElement>("#app");
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="shell">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<p class="eyebrow">Vite + TypeScript</p>
|
||||
<h1>TypeScript Learning Lab</h1>
|
||||
<p class="intro">
|
||||
保留原来的 8 个练习目录,同时给这一章补上一个可直接运行的学习面板。
|
||||
</p>
|
||||
</div>
|
||||
<nav class="lesson-nav" aria-label="TypeScript lessons"></nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<section class="hero card">
|
||||
<p class="badge">06-typescript</p>
|
||||
<h2 id="lesson-title"></h2>
|
||||
<p id="lesson-focus" class="focus"></p>
|
||||
<p id="lesson-summary" class="summary"></p>
|
||||
</section>
|
||||
<section class="grid">
|
||||
<article class="card">
|
||||
<div class="card-head">
|
||||
<h3>知识点</h3>
|
||||
</div>
|
||||
<ul id="lesson-points" class="point-list"></ul>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="card-head">
|
||||
<h3>运行结果</h3>
|
||||
<button id="run-demo" class="run-button" type="button">运行演示</button>
|
||||
</div>
|
||||
<div id="lesson-output" class="output-panel"></div>
|
||||
</article>
|
||||
</section>
|
||||
<section class="grid code-grid">
|
||||
<article class="card code-card">
|
||||
<div class="card-head">
|
||||
<h3>Starter</h3>
|
||||
</div>
|
||||
<pre><code id="starter-code"></code></pre>
|
||||
</article>
|
||||
<article class="card code-card">
|
||||
<div class="card-head">
|
||||
<h3>Answer</h3>
|
||||
</div>
|
||||
<pre><code id="answer-code"></code></pre>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const nav = getRequiredElement<HTMLElement>(".lesson-nav");
|
||||
const title = getRequiredElement<HTMLElement>("#lesson-title");
|
||||
const focus = getRequiredElement<HTMLElement>("#lesson-focus");
|
||||
const summary = getRequiredElement<HTMLElement>("#lesson-summary");
|
||||
const pointList = getRequiredElement<HTMLElement>("#lesson-points");
|
||||
const output = getRequiredElement<HTMLElement>("#lesson-output");
|
||||
const starterCode = getRequiredElement<HTMLElement>("#starter-code");
|
||||
const answerCode = getRequiredElement<HTMLElement>("#answer-code");
|
||||
const runButton = getRequiredElement<HTMLButtonElement>("#run-demo");
|
||||
|
||||
let activeLessonId = lessons[0]?.id ?? "";
|
||||
|
||||
function renderLessonList(): void {
|
||||
nav.innerHTML = lessons
|
||||
.map((lesson) => {
|
||||
const isActive = lesson.id === activeLessonId;
|
||||
return `
|
||||
<button
|
||||
class="lesson-link ${isActive ? "is-active" : ""}"
|
||||
type="button"
|
||||
data-id="${lesson.id}"
|
||||
>
|
||||
<span class="lesson-index">${lesson.id}</span>
|
||||
<span class="lesson-meta">
|
||||
<strong>${lesson.title}</strong>
|
||||
<small>${lesson.focus}</small>
|
||||
</span>
|
||||
</button>
|
||||
`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderOutput(lines: string[]): void {
|
||||
output.innerHTML = lines
|
||||
.map((line) => `<div class="output-line">${line}</div>`)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderActiveLesson(): void {
|
||||
const lesson = lessons.find((item) => item.id === activeLessonId);
|
||||
|
||||
if (!lesson) {
|
||||
return;
|
||||
}
|
||||
|
||||
title.textContent = `${lesson.id}. ${lesson.title}`;
|
||||
focus.textContent = lesson.focus;
|
||||
summary.textContent = lesson.summary;
|
||||
pointList.innerHTML = lesson.keyPoints
|
||||
.map((item) => `<li>${item}</li>`)
|
||||
.join("");
|
||||
starterCode.textContent = lesson.starterCode;
|
||||
answerCode.textContent = lesson.answerCode;
|
||||
renderOutput(lesson.runDemo());
|
||||
renderLessonList();
|
||||
}
|
||||
|
||||
nav.addEventListener("click", (event) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const button = target.closest<HTMLButtonElement>("[data-id]");
|
||||
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeLessonId = button.dataset.id ?? activeLessonId;
|
||||
renderActiveLesson();
|
||||
});
|
||||
|
||||
runButton.addEventListener("click", () => {
|
||||
const lesson = lessons.find((item) => item.id === activeLessonId);
|
||||
|
||||
if (!lesson) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderOutput(lesson.runDemo());
|
||||
});
|
||||
|
||||
renderActiveLesson();
|
||||
237
06-typescript/src/style.css
Normal file
237
06-typescript/src/style.css
Normal file
@@ -0,0 +1,237 @@
|
||||
:root {
|
||||
font-family: "IBM Plex Sans", "PingFang SC", "Hiragino Sans GB", sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
color: #f7f4ec;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 191, 73, 0.22), transparent 24rem),
|
||||
radial-gradient(circle at bottom right, rgba(86, 154, 255, 0.16), transparent 26rem),
|
||||
#11131a;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
button,
|
||||
code,
|
||||
pre {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.shell {
|
||||
display: grid;
|
||||
grid-template-columns: 320px minmax(0, 1fr);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding: 28px 20px;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(9, 10, 14, 0.62);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.brand {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.eyebrow,
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 0 12px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 191, 73, 0.14);
|
||||
color: #ffcc6a;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.brand h1 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 28px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.intro,
|
||||
.summary {
|
||||
margin: 0;
|
||||
color: rgba(247, 244, 236, 0.72);
|
||||
}
|
||||
|
||||
.lesson-nav {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.lesson-link {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 14px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 18px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 180ms ease,
|
||||
border-color 180ms ease,
|
||||
background 180ms ease;
|
||||
}
|
||||
|
||||
.lesson-link:hover,
|
||||
.lesson-link.is-active {
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(255, 191, 73, 0.42);
|
||||
background: rgba(255, 191, 73, 0.1);
|
||||
}
|
||||
|
||||
.lesson-index {
|
||||
min-width: 40px;
|
||||
padding: 8px 0;
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.lesson-meta {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.lesson-meta small {
|
||||
color: rgba(247, 244, 236, 0.64);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.focus {
|
||||
margin: 0 0 10px;
|
||||
color: #ffcc6a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.code-grid {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 28px;
|
||||
background: rgba(15, 17, 24, 0.72);
|
||||
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.22);
|
||||
}
|
||||
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.card-head h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.point-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
color: rgba(247, 244, 236, 0.84);
|
||||
}
|
||||
|
||||
.point-list li + li {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.output-panel {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
background: rgba(86, 154, 255, 0.12);
|
||||
color: #d8e9ff;
|
||||
}
|
||||
|
||||
.run-button {
|
||||
padding: 10px 14px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(135deg, #ffbf49, #ff8f48);
|
||||
color: #1d1302;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.code-card pre {
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
border-radius: 20px;
|
||||
background: #0b0d12;
|
||||
}
|
||||
|
||||
.code-card code {
|
||||
font-family: "JetBrains Mono", "SFMono-Regular", monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
1
06-typescript/src/vite-env.d.ts
vendored
Normal file
1
06-typescript/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user