lazyslides 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -26,7 +26,7 @@ LazySlides separates them. You write slides as structured YAML — the format AI
26
26
 
27
27
  ## Features
28
28
 
29
- - **17 slide templates** — title, content, metrics, comparison, timeline, funnel, code, and more
29
+ - **19 slide templates** — title, content, metrics, comparison, diagram, timeline, funnel, code, and more
30
30
  - **Speaker view** — press S for notes, timer, and next-slide preview
31
31
  - **Section progress bar** — visual progress dots with section markers; click to jump between sections
32
32
  - **Source references** — attach citations to any slide, rendered as linked footnotes
@@ -128,7 +128,7 @@ Supporting files (outlines, notes, PDFs, images) go alongside `index.md` in the
128
128
 
129
129
  ## Templates
130
130
 
131
- 17 built-in templates cover common slide patterns:
131
+ 19 built-in templates cover common slide patterns:
132
132
 
133
133
  | Template | Description |
134
134
  |----------|-------------|
@@ -149,6 +149,7 @@ Supporting files (outlines, notes, PDFs, images) go alongside `index.md` in the
149
149
  | `code` | Code snippet with syntax highlighting |
150
150
  | `image-overlay` | Full image with positioned text box |
151
151
  | `agenda` | Clickable table of contents (supports `auto_generate: true`) |
152
+ | `diagram` | D2 diagram compiled to inline SVG at build time |
152
153
 
153
154
  See `CLAUDE.md` for the full field reference for each template.
154
155
 
@@ -0,0 +1,34 @@
1
+ {# TEMPLATE: diagram — D2 diagram rendered to inline SVG at build time.
2
+ Fields:
3
+ title* — slide title
4
+ d2 — inline D2 source (multiline with |)
5
+ d2_file — path to .d2 file (alternative to inline d2)
6
+ caption — optional description below diagram
7
+ notes — speaker notes
8
+ reference / reference_link / references — source attribution
9
+ #}
10
+ {% from "slides/_section-attrs.njk" import sectionAttrs %}
11
+ <section class="slide-diagram"{{ sectionAttrs(slide) }}>
12
+ <div class="slide-body">
13
+ <div class="slide-header">
14
+ <h2>{{ slide.title }}</h2>
15
+ </div>
16
+ <div class="diagram-container">
17
+ {% if slide.d2 %}
18
+ {{ slide.d2 | compileD2 | safe }}
19
+ {% elif slide.d2_file %}
20
+ <div class="d2-error">d2_file is not yet supported. Use inline d2 instead.</div>
21
+ {% else %}
22
+ <div class="d2-error">No d2 source provided. Add a 'd2' or 'd2_file' field.</div>
23
+ {% endif %}
24
+ </div>
25
+ {% if slide.caption %}<p class="diagram-caption">{{ slide.caption }}</p>{% endif %}
26
+ {% if slide.notes %}<aside class="notes">{{ slide.notes }}</aside>{% endif %}
27
+ </div>
28
+ {% if global_footer and not slide.hide_footer %}
29
+ {% set reference = slide.reference %}
30
+ {% set reference_link = slide.reference_link %}
31
+ {% set references = slide.references %}
32
+ {% include "slides/footer.njk" %}
33
+ {% endif %}
34
+ </section>
package/index.js CHANGED
@@ -23,6 +23,97 @@ export default function lazyslides(eleventyConfig, options = {}) {
23
23
  return val !== null && typeof val === "object" && !Array.isArray(val);
24
24
  });
25
25
 
26
+ // ---------------------------------------------------------------
27
+ // 1b. D2 diagram compilation (async filter)
28
+ // ---------------------------------------------------------------
29
+ let d2Instance = null;
30
+ let d2Available = null; // null = unknown, true/false after check
31
+ async function getD2() {
32
+ if (d2Available === false) return null;
33
+ if (!d2Instance) {
34
+ try {
35
+ const { D2 } = await import("@terrastruct/d2");
36
+ d2Instance = new D2();
37
+ d2Available = true;
38
+ } catch {
39
+ d2Available = false;
40
+ return null;
41
+ }
42
+ }
43
+ return d2Instance;
44
+ }
45
+
46
+ // Pre-compile D2 diagrams before Nunjucks renders, because Nunjucks
47
+ // async filters/shortcodes are unreliable inside {% for %} loops.
48
+ // We store compiled SVGs in a map keyed by source hash, then look them
49
+ // up synchronously via a regular filter during template rendering.
50
+ const d2Cache = new Map();
51
+
52
+ eleventyConfig.addFilter("compileD2", (source) => {
53
+ if (!source) return "";
54
+ const cached = d2Cache.get(source);
55
+ if (cached) return cached;
56
+ return `<div class="d2-error">D2 diagram was not pre-compiled.</div>`;
57
+ });
58
+
59
+ eleventyConfig.on("eleventy.before", async ({ directories }) => {
60
+ // Scan all presentation files for D2 source blocks and pre-compile them
61
+ const presentationsDir = path.join(directories.input || ".", "presentations");
62
+ if (!fs.existsSync(presentationsDir)) return;
63
+
64
+ const { default: yaml } = await import("js-yaml");
65
+ const entries = fs.readdirSync(presentationsDir, { withFileTypes: true });
66
+ const d2Sources = [];
67
+
68
+ for (const entry of entries) {
69
+ if (!entry.isDirectory() || entry.name === "_template") continue;
70
+ const indexPath = path.join(presentationsDir, entry.name, "index.md");
71
+ if (!fs.existsSync(indexPath)) continue;
72
+
73
+ const content = fs.readFileSync(indexPath, "utf-8");
74
+ const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
75
+ if (!fmMatch) continue;
76
+
77
+ try {
78
+ const data = yaml.load(fmMatch[1]);
79
+ if (!data?.slides) continue;
80
+ for (const slide of data.slides) {
81
+ if (slide.template === "diagram" && slide.d2) {
82
+ d2Sources.push(slide.d2);
83
+ }
84
+ }
85
+ } catch { /* skip invalid YAML */ }
86
+ }
87
+
88
+ if (d2Sources.length === 0) return;
89
+
90
+ const d2 = await getD2();
91
+ if (!d2) {
92
+ console.log("[LazySlides] @terrastruct/d2 not installed — diagram slides will show placeholders. Install it with: pnpm add @terrastruct/d2");
93
+ for (const source of d2Sources) {
94
+ d2Cache.set(source, `<div class="d2-error">@terrastruct/d2 is not installed. Run: pnpm add @terrastruct/d2</div>`);
95
+ }
96
+ return;
97
+ }
98
+ for (const source of d2Sources) {
99
+ try {
100
+ const result = await d2.compile(source);
101
+ const svg = await d2.render(result.diagram, {
102
+ ...result.renderOptions,
103
+ noXMLTag: true,
104
+ pad: 20,
105
+ });
106
+ d2Cache.set(source, svg);
107
+ } catch (err) {
108
+ const safeMsg = (err.message || String(err))
109
+ .replace(/&/g, "&amp;")
110
+ .replace(/</g, "&lt;")
111
+ .replace(/>/g, "&gt;");
112
+ d2Cache.set(source, `<div class="d2-error">${safeMsg}</div>`);
113
+ }
114
+ }
115
+ });
116
+
26
117
  // ---------------------------------------------------------------
27
118
  // 2. Nunjucks search paths — makes {% include "slides/…" %} and
28
119
  // layout: presentation.njk resolve to files inside the package.
package/lib/export-pdf.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execSync, spawn } from "node:child_process";
1
+ import { spawn } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import http from "node:http";
@@ -6,14 +6,18 @@ import http from "node:http";
6
6
  const PORT = 4100;
7
7
  const BASE_URL = `http://127.0.0.1:${PORT}`;
8
8
  const OUTPUT_DIR = "_pdfs";
9
- const TIMEOUT_SECONDS = 30;
9
+ const SERVER_TIMEOUT_SECONDS = 60;
10
+ const BUILD_IDLE_TIMEOUT_MS = 15000;
11
+ const BUILD_HARD_TIMEOUT_MS = 180000;
10
12
 
11
13
  let serverProcess = null;
12
14
 
13
15
  function cleanup() {
14
16
  if (serverProcess) {
15
17
  console.log("\n Stopping server...");
16
- serverProcess.kill();
18
+ try {
19
+ serverProcess.kill("SIGTERM");
20
+ } catch {}
17
21
  serverProcess = null;
18
22
  }
19
23
  }
@@ -29,9 +33,9 @@ function waitForServer() {
29
33
  });
30
34
  req.on("error", () => {
31
35
  elapsed++;
32
- if (elapsed >= TIMEOUT_SECONDS) {
36
+ if (elapsed >= SERVER_TIMEOUT_SECONDS) {
33
37
  clearInterval(interval);
34
- reject(new Error(`Server failed to start within ${TIMEOUT_SECONDS}s`));
38
+ reject(new Error(`Server failed to start within ${SERVER_TIMEOUT_SECONDS}s`));
35
39
  }
36
40
  });
37
41
  req.end();
@@ -39,29 +43,107 @@ function waitForServer() {
39
43
  });
40
44
  }
41
45
 
42
- function runDecktape(name) {
43
- const url = `${BASE_URL}/presentations/${name}/?pdf`;
44
- const output = path.join(OUTPUT_DIR, `${name}.pdf`);
46
+ /**
47
+ * Run `pnpm run build`, but tolerate the eleventy child not exiting cleanly
48
+ * (known issue: d2 WASM / chokidar handles can keep the event loop alive after
49
+ * "Wrote N files"). We detect idle stdout and force-kill.
50
+ */
51
+ function runBuild(cwd) {
52
+ return new Promise((resolve, reject) => {
53
+ // Pipe stdio so we can observe activity and reset the idle timer.
54
+ const child = spawn("pnpm", ["run", "build"], {
55
+ cwd,
56
+ stdio: ["ignore", "pipe", "pipe"],
57
+ });
58
+
59
+ let idleTimer = null;
60
+ let hardTimer = null;
61
+ let resolved = false;
62
+
63
+ const done = (err) => {
64
+ if (resolved) return;
65
+ resolved = true;
66
+ clearTimeout(idleTimer);
67
+ clearTimeout(hardTimer);
68
+ try { child.kill("SIGKILL"); } catch {}
69
+ err ? reject(err) : resolve();
70
+ };
71
+
72
+ const bumpIdle = () => {
73
+ clearTimeout(idleTimer);
74
+ idleTimer = setTimeout(() => {
75
+ console.log(`\n Build child idle for ${BUILD_IDLE_TIMEOUT_MS / 1000}s — assuming done and force-exiting.`);
76
+ done();
77
+ }, BUILD_IDLE_TIMEOUT_MS);
78
+ };
79
+
80
+ child.stdout.on("data", (chunk) => {
81
+ process.stdout.write(chunk);
82
+ bumpIdle();
83
+ });
84
+ child.stderr.on("data", (chunk) => {
85
+ process.stderr.write(chunk);
86
+ bumpIdle();
87
+ });
88
+
89
+ bumpIdle();
90
+
91
+ hardTimer = setTimeout(() => {
92
+ done(new Error(`Build exceeded hard timeout ${BUILD_HARD_TIMEOUT_MS / 1000}s`));
93
+ }, BUILD_HARD_TIMEOUT_MS);
94
+
95
+ child.on("exit", (code) => {
96
+ if (code === 0 || code === null) done();
97
+ else done(new Error(`Build failed with exit code ${code}`));
98
+ });
99
+ child.on("error", (err) => done(err));
100
+ });
101
+ }
45
102
 
46
- console.log(` → Exporting: ${name}`);
47
- try {
48
- execSync(
49
- `npx decktape reveal --size 1920x1080 --pause 1000 --load-pause 2000 "${url}" "${output}"`,
50
- { stdio: ["ignore", "pipe", "pipe"] }
103
+ function runDecktape(name, cwd) {
104
+ return new Promise((resolve) => {
105
+ const url = `${BASE_URL}/presentations/${name}/?pdf`;
106
+ const output = path.join(OUTPUT_DIR, `${name}.pdf`);
107
+
108
+ console.log(` → Exporting: ${name}`);
109
+ console.log(` URL: ${url}`);
110
+ console.log(` Output: ${output}\n`);
111
+
112
+ const child = spawn(
113
+ "npx",
114
+ [
115
+ "decktape",
116
+ "reveal",
117
+ "--size", "1920x1080",
118
+ "--pause", "1000",
119
+ "--load-pause", "2000",
120
+ url,
121
+ output,
122
+ ],
123
+ { cwd, stdio: ["ignore", "inherit", "inherit"] }
51
124
  );
52
- console.log(` \u2713 ${output}`);
53
- return true;
54
- } catch {
55
- console.log(` \u2717 Failed: ${name}`);
56
- return false;
57
- }
125
+
126
+ child.on("exit", (code) => {
127
+ if (code === 0) {
128
+ console.log(`\n \u2713 ${output}`);
129
+ resolve(true);
130
+ } else {
131
+ console.log(`\n \u2717 Failed: ${name} (decktape exit code ${code})`);
132
+ resolve(false);
133
+ }
134
+ });
135
+ child.on("error", (err) => {
136
+ console.log(`\n \u2717 Failed: ${name} (${err.message})`);
137
+ resolve(false);
138
+ });
139
+ });
58
140
  }
59
141
 
60
- function getAvailablePresentations(cwd) {
142
+ function getAvailablePresentations(cwd, { includeHidden = false } = {}) {
61
143
  const dir = path.join(cwd, "presentations");
62
144
  return fs
63
145
  .readdirSync(dir, { withFileTypes: true })
64
- .filter((d) => d.isDirectory() && d.name !== "_template")
146
+ .filter((d) => d.isDirectory() && (includeHidden || !d.name.startsWith("_")))
65
147
  .map((d) => d.name);
66
148
  }
67
149
 
@@ -75,9 +157,10 @@ export async function run(opts = {}) {
75
157
  const cwd = opts.cwd || process.cwd();
76
158
  const requestedName = opts.name;
77
159
  const available = getAvailablePresentations(cwd);
160
+ const allForLookup = getAvailablePresentations(cwd, { includeHidden: true });
78
161
 
79
162
  if (requestedName) {
80
- if (!available.includes(requestedName)) {
163
+ if (!allForLookup.includes(requestedName)) {
81
164
  console.error(`Presentation not found: ${requestedName}`);
82
165
  console.error(" Available presentations:");
83
166
  for (const name of available) {
@@ -89,16 +172,25 @@ export async function run(opts = {}) {
89
172
 
90
173
  const presentations = requestedName ? [requestedName] : available;
91
174
 
92
- // Build the site
175
+ // Build the site (tolerate lingering child handles)
93
176
  console.log("Building site...");
94
- execSync("pnpm run build", { stdio: "inherit", cwd });
177
+ try {
178
+ await runBuild(cwd);
179
+ } catch (err) {
180
+ console.error(`Build failed: ${err.message}`);
181
+ process.exit(1);
182
+ }
95
183
 
96
184
  // Start server
97
185
  console.log(`\nStarting server on port ${PORT}...`);
98
186
  serverProcess = spawn("npx", ["eleventy", "--serve", "--port", String(PORT)], {
99
- stdio: "ignore",
187
+ stdio: ["ignore", "pipe", "pipe"],
100
188
  cwd,
101
189
  });
190
+ serverProcess.stdout.on("data", () => {}); // drain
191
+ serverProcess.stderr.on("data", (chunk) => {
192
+ process.stderr.write(`[server] ${chunk}`);
193
+ });
102
194
 
103
195
  process.on("exit", cleanup);
104
196
  process.on("SIGINT", () => { cleanup(); process.exit(1); });
@@ -106,7 +198,13 @@ export async function run(opts = {}) {
106
198
 
107
199
  // Wait for server
108
200
  console.log("Waiting for server...");
109
- await waitForServer();
201
+ try {
202
+ await waitForServer();
203
+ } catch (err) {
204
+ console.error(`\n${err.message}`);
205
+ cleanup();
206
+ process.exit(1);
207
+ }
110
208
  console.log("Server ready");
111
209
 
112
210
  // Create output directory
@@ -118,7 +216,7 @@ export async function run(opts = {}) {
118
216
  console.log(`\nExporting ${presentations.length} presentation(s) to PDF...\n`);
119
217
 
120
218
  for (const name of presentations) {
121
- if (runDecktape(name)) {
219
+ if (await runDecktape(name, cwd)) {
122
220
  success++;
123
221
  } else {
124
222
  failure++;
package/lib/validate.js CHANGED
@@ -7,6 +7,7 @@ const VALID_TEMPLATES = [
7
7
  "title", "section", "content", "metrics", "comparison", "columns",
8
8
  "three-columns", "quote", "center", "hero", "image-overlay", "code",
9
9
  "timeline", "funnel", "split", "split-wide", "table", "agenda",
10
+ "diagram",
10
11
  ];
11
12
  const VALID_TRANSITIONS = [
12
13
  "none", "fade", "slide", "convex", "concave", "zoom",
@@ -128,6 +129,16 @@ export async function run(opts = {}) {
128
129
  }
129
130
  }
130
131
 
132
+ // Diagram template validation
133
+ if (slide.template === "diagram") {
134
+ if (!slide.d2 && !slide.d2_file) {
135
+ errors.push(`${file}: Slide ${slideNum} (diagram) missing 'd2' or 'd2_file' — one is required`);
136
+ }
137
+ if (slide.d2 && slide.d2_file) {
138
+ errors.push(`${file}: Slide ${slideNum} (diagram) has both 'd2' and 'd2_file' — use only one`);
139
+ }
140
+ }
141
+
131
142
  // Image path validation
132
143
  if (slide.image) {
133
144
  const imagePath = slide.image;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazyslides",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "description": "Slide decks with native agentic AI integration",
5
5
  "license": "MIT",
6
6
  "author": "Chris Tietz",
@@ -50,15 +50,18 @@
50
50
  "@11ty/eleventy": "^3.0.0"
51
51
  },
52
52
  "dependencies": {
53
+ "decktape": "^3.14.0",
53
54
  "js-yaml": "^4.1.0"
54
55
  },
56
+ "optionalDependencies": {
57
+ "@terrastruct/d2": "^0.1.33"
58
+ },
55
59
  "devDependencies": {
56
60
  "@11ty/eleventy": "^3.0.0",
57
- "@tailwindcss/cli": "^4.0.0",
61
+ "@tailwindcss/cli": "4.1.18",
58
62
  "concurrently": "^9.0.0",
59
- "decktape": "^3.14.0",
60
- "tailwindcss": "^4.1.18",
61
- "vitest": "^4.0.18"
63
+ "tailwindcss": "4.1.18",
64
+ "vitest": "^4.1.2"
62
65
  },
63
66
  "scripts": {
64
67
  "dev": "node cli.js validate && concurrently \"pnpm:watch:*\"",
@@ -190,6 +190,15 @@ Fields marked with `*` are required.
190
190
  - `slide`* — 0-based slide index to link to
191
191
  - `reference` / `reference_link` / `references`
192
192
 
193
+ #### `diagram` — D2 diagram (compiled to inline SVG at build time)
194
+ - `title`* — slide title
195
+ - `d2` — inline D2 source string (use `|` for multiline YAML)
196
+ - `d2_file` — path to a `.d2` file (alternative to inline `d2`; not yet supported)
197
+ - `caption` — optional description below diagram
198
+ - `reference` / `reference_link` / `references`
199
+
200
+ Note: requires `@terrastruct/d2` npm dependency (included in the package). Exactly one of `d2` or `d2_file` must be provided.
201
+
193
202
  ## Common Patterns
194
203
 
195
204
  ### Nested lists
@@ -41,6 +41,7 @@ Ask the user which template to use. Show this reference:
41
41
  | `code` | Code snippet with syntax highlighting |
42
42
  | `image-overlay` | Full image with positioned text box |
43
43
  | `agenda` | Clickable table of contents |
44
+ | `diagram` | D2 diagram compiled to inline SVG |
44
45
 
45
46
  ## Step 4: Gather Content
46
47
 
@@ -11,7 +11,7 @@ Ask the user:
11
11
 
12
12
  ## Step 2: Choose a Name
13
13
 
14
- Help the user pick a kebab-case name for the template (e.g. `icon-grid`, `two-images`, `photo-grid`). The name must not conflict with the 17 built-in templates: `title`, `section`, `content`, `center`, `hero`, `metrics`, `comparison`, `columns`, `quote`, `image-overlay`, `code`, `timeline`, `funnel`, `split`, `split-wide`, `table`, `agenda`.
14
+ Help the user pick a kebab-case name for the template (e.g. `icon-grid`, `two-images`, `photo-grid`). The name must not conflict with the 19 built-in templates: `title`, `section`, `content`, `center`, `hero`, `metrics`, `comparison`, `columns`, `quote`, `image-overlay`, `code`, `timeline`, `funnel`, `split`, `split-wide`, `table`, `agenda`, `diagram`.
15
15
 
16
16
  ## Step 3: Pick a Starting Point
17
17
 
@@ -90,6 +90,7 @@ Review each slide in the outline and propose a template. The available templates
90
90
  | `table` | Data table with headers |
91
91
  | `code` | Code snippet with syntax highlighting |
92
92
  | `image-overlay` | Image with text overlay |
93
+ | `diagram` | D2 diagram compiled to inline SVG |
93
94
 
94
95
  Read each template file's docblock comment for exact YAML structure and options.
95
96
 
@@ -19,7 +19,7 @@ Review the output and categorize issues:
19
19
  - **Wrong comparison format** — comparison slides must use `rows:` with `left`/`right` pairs
20
20
 
21
21
  ### Warnings (should fix)
22
- - **Unknown template** — template name doesn't match one of the 17 valid types
22
+ - **Unknown template** — template name doesn't match one of the 19 valid types
23
23
  - **Missing title** — slide has no title (except `quote` which doesn't need one)
24
24
  - **Missing image** — image path in YAML doesn't exist on disk
25
25
  - **Hidden slide** — possible slide definition commented out
package/src/styles.css CHANGED
@@ -2235,3 +2235,67 @@ html, body {
2235
2235
  .section-progress-dots .section-dot:hover::after {
2236
2236
  opacity: 1;
2237
2237
  }
2238
+
2239
+ /* ============================================
2240
+ SLIDE: DIAGRAM
2241
+ D2 diagrams rendered to inline SVG
2242
+ ============================================ */
2243
+
2244
+ .reveal .slides section.slide-diagram {
2245
+ display: flex !important;
2246
+ flex-direction: column;
2247
+ justify-content: flex-start;
2248
+ align-items: stretch;
2249
+ height: 100%;
2250
+ padding: 0;
2251
+ }
2252
+
2253
+ .reveal .slides section.slide-diagram .slide-body {
2254
+ flex: 1;
2255
+ padding: var(--slide-padding);
2256
+ padding-bottom: 12px;
2257
+ display: flex;
2258
+ flex-direction: column;
2259
+ min-height: 0;
2260
+ }
2261
+
2262
+ .reveal .slides section.slide-diagram .slide-header {
2263
+ margin-bottom: 12px;
2264
+ padding-bottom: 10px;
2265
+ border-bottom: 2px solid var(--color-primary-500);
2266
+ }
2267
+
2268
+ .reveal .slides section.slide-diagram .diagram-container {
2269
+ display: flex;
2270
+ justify-content: center;
2271
+ align-items: center;
2272
+ flex: 1;
2273
+ overflow: hidden;
2274
+ min-height: 0;
2275
+ }
2276
+
2277
+ .reveal .slides section.slide-diagram .diagram-container svg {
2278
+ max-width: 100%;
2279
+ max-height: 100%;
2280
+ height: auto;
2281
+ width: auto;
2282
+ }
2283
+
2284
+ .reveal .slides section.slide-diagram .diagram-caption {
2285
+ text-align: center;
2286
+ font-size: 0.75em;
2287
+ color: var(--color-text-muted);
2288
+ margin-top: 0.5em;
2289
+ }
2290
+
2291
+ .reveal .slides section.slide-diagram .d2-error {
2292
+ color: #dc2626;
2293
+ background: #fef2f2;
2294
+ border: 1px solid #fecaca;
2295
+ border-radius: 8px;
2296
+ padding: 1em;
2297
+ font-family: var(--font-mono);
2298
+ font-size: 0.7em;
2299
+ white-space: pre-wrap;
2300
+ text-align: left;
2301
+ }