@cyber-dash-tech/revela 0.3.0 → 0.4.0
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 +200 -9
- package/README.zh-CN.md +162 -9
- package/designs/summit/DESIGN.md +54 -51
- package/lib/agents/research-prompt.ts +1 -1
- package/lib/document-materials/extract.ts +139 -2
- package/lib/media/batch-save.ts +146 -0
- package/lib/media/download.ts +68 -0
- package/lib/media/save.ts +273 -0
- package/lib/media/types.ts +54 -0
- package/lib/research/image-leads.ts +175 -0
- package/package.json +1 -1
- package/plugin.ts +6 -0
- package/skill/SKILL.md +7 -1
- package/tools/extract-document-materials.ts +2 -2
- package/tools/media-batch-save.ts +25 -0
- package/tools/media-save.ts +40 -0
- package/tools/research-images-list.ts +34 -0
package/designs/summit/DESIGN.md
CHANGED
|
@@ -33,6 +33,9 @@ Apply this visual style when generating all slides in this session.
|
|
|
33
33
|
--shadow-soft: rgba(0, 0, 0, 0.18);
|
|
34
34
|
--font-display: 'IBM Plex Sans Condensed', 'Inter', ui-sans-serif, sans-serif;
|
|
35
35
|
--font-body: 'Inter', ui-sans-serif, sans-serif;
|
|
36
|
+
--font-size-body: 17px;
|
|
37
|
+
--font-size-meta: 17px;
|
|
38
|
+
--font-size-body-strong: 20px;
|
|
36
39
|
}
|
|
37
40
|
```
|
|
38
41
|
|
|
@@ -58,10 +61,11 @@ Accent usage guidance:
|
|
|
58
61
|
- inner-layout h2: `30px` to `36px`, weight `600` to `700`, line-height `1.06` to `1.12`
|
|
59
62
|
- inner-layout h3: `20px` to `24px`, weight `600`, line-height `1.12` to `1.18`
|
|
60
63
|
- Body: `17px`, line-height `1.6`
|
|
61
|
-
- Eyebrow / caption
|
|
64
|
+
- Eyebrow / caption / metadata: `17px`, uppercase, letter-spacing `0.12em` to `0.18em`
|
|
62
65
|
- Stat number: `72px` to `88px`, weight `500`, line-height `0.95`
|
|
63
66
|
- Never use text shadows or glow.
|
|
64
67
|
- Never switch to a serif typeface; Summit is strictly sans-serif.
|
|
68
|
+
- Foundation owns the default text scale. In components, let `p`, `li`, `.caption`, `.eyebrow`, and `h3` inherit the foundation sizes unless a component has a clear structural reason to differ.
|
|
65
69
|
|
|
66
70
|
All sizes are fixed `px` for the 1920x1080 canvas. JS `transform: scale()` handles viewport adaptation. Never use `clamp()` or viewport-relative units.
|
|
67
71
|
|
|
@@ -196,9 +200,9 @@ body {
|
|
|
196
200
|
.eyebrow,
|
|
197
201
|
.caption,
|
|
198
202
|
.meta-label {
|
|
199
|
-
font-size:
|
|
203
|
+
font-size: var(--font-size-meta);
|
|
200
204
|
line-height: 1.4;
|
|
201
|
-
letter-spacing: 0.
|
|
205
|
+
letter-spacing: 0.14em;
|
|
202
206
|
text-transform: uppercase;
|
|
203
207
|
color: var(--text-muted);
|
|
204
208
|
}
|
|
@@ -215,7 +219,7 @@ h2 { font-size: 34px; line-height: 1.08; }
|
|
|
215
219
|
h3 { font-size: 24px; line-height: 1.14; }
|
|
216
220
|
|
|
217
221
|
p, li {
|
|
218
|
-
font-size:
|
|
222
|
+
font-size: var(--font-size-body);
|
|
219
223
|
line-height: 1.6;
|
|
220
224
|
color: var(--text-secondary);
|
|
221
225
|
}
|
|
@@ -234,8 +238,8 @@ p, li {
|
|
|
234
238
|
display: inline-flex;
|
|
235
239
|
align-items: center;
|
|
236
240
|
gap: 10px;
|
|
237
|
-
font-size:
|
|
238
|
-
letter-spacing: 0.
|
|
241
|
+
font-size: var(--font-size-meta);
|
|
242
|
+
letter-spacing: 0.14em;
|
|
239
243
|
text-transform: uppercase;
|
|
240
244
|
color: var(--text-muted);
|
|
241
245
|
}
|
|
@@ -262,7 +266,7 @@ p, li {
|
|
|
262
266
|
|
|
263
267
|
.media-caption {
|
|
264
268
|
margin-top: 12px;
|
|
265
|
-
font-size:
|
|
269
|
+
font-size: var(--font-size-meta);
|
|
266
270
|
line-height: 1.5;
|
|
267
271
|
letter-spacing: 0.14em;
|
|
268
272
|
text-transform: uppercase;
|
|
@@ -282,7 +286,7 @@ p, li {
|
|
|
282
286
|
.editorial-list li {
|
|
283
287
|
position: relative;
|
|
284
288
|
padding-left: 20px;
|
|
285
|
-
font-size:
|
|
289
|
+
font-size: var(--font-size-body);
|
|
286
290
|
line-height: 1.58;
|
|
287
291
|
color: var(--text-secondary);
|
|
288
292
|
}
|
|
@@ -773,7 +777,7 @@ Unified narrative text container. Use inside any layout slot that needs a self-c
|
|
|
773
777
|
<p class="eyebrow" style="color:rgba(243,238,230,0.72);">Section label / annual review</p>
|
|
774
778
|
<h2 style="margin-top:16px;font-size:60px;line-height:0.92;letter-spacing:-0.03em;text-transform:uppercase;color:#f7f4ee;max-width:360px;">Narrative heading</h2>
|
|
775
779
|
<div class="text-panel-body" style="margin-top:20px;">
|
|
776
|
-
<p style="
|
|
780
|
+
<p style="color:rgba(243,238,230,0.84);max-width:390px;">Use one or two compact paragraphs when continuous prose fits the content better than a list.</p>
|
|
777
781
|
</div>
|
|
778
782
|
</div>
|
|
779
783
|
<div class="text-panel-footer" style="color:rgba(243,238,230,0.68);">
|
|
@@ -807,7 +811,7 @@ Unified narrative text container. Use inside any layout slot that needs a self-c
|
|
|
807
811
|
<p class="eyebrow" style="color:rgba(243,238,230,0.72);">Context</p>
|
|
808
812
|
<h3 style="margin-top:12px;color:#f7f4ee;">Heading here</h3>
|
|
809
813
|
<div class="text-panel-body" style="margin-top:16px;">
|
|
810
|
-
<p style="
|
|
814
|
+
<p style="color:rgba(243,238,230,0.84);">Introductory sentence that frames what follows. Then the list expands the argument:</p>
|
|
811
815
|
<ul class="editorial-list" style="margin-top:12px;">
|
|
812
816
|
<li><strong>Lead phrase.</strong> Supporting explanation.</li>
|
|
813
817
|
<li><strong>Second point.</strong> One sentence of evidence.</li>
|
|
@@ -884,8 +888,8 @@ This is **not** a dashboard card. It has no border, no background fill, no shado
|
|
|
884
888
|
<p class="caption" style="color:var(--text-muted);">Performance signal</p>
|
|
885
889
|
<div class="stat-card-value" style="color: var(--accent-gold);">27%</div>
|
|
886
890
|
<div class="text-panel-body" style="gap:10px;max-width:330px;">
|
|
887
|
-
<h3 style="
|
|
888
|
-
<p
|
|
891
|
+
<h3 style="line-height:1.04;text-transform:uppercase;">EBIT Margin</h3>
|
|
892
|
+
<p>Expanded for the third consecutive quarter as premium mix offset freight pressure and held pricing discipline through softer volume.</p>
|
|
889
893
|
</div>
|
|
890
894
|
</div>
|
|
891
895
|
|
|
@@ -896,8 +900,8 @@ This is **not** a dashboard card. It has no border, no background fill, no shado
|
|
|
896
900
|
<div class="stat-card-value" style="color: var(--accent-olive);">4.8x</div>
|
|
897
901
|
</div>
|
|
898
902
|
<div class="text-panel-body" style="gap:10px;max-width:330px;">
|
|
899
|
-
<h3 style="
|
|
900
|
-
<p
|
|
903
|
+
<h3 style="line-height:1.04;text-transform:uppercase;">Inventory Turnover</h3>
|
|
904
|
+
<p>Higher cycle efficiency reduced working-capital drag without adding new capacity, leaving more headroom for seasonal demand swings.</p>
|
|
901
905
|
</div>
|
|
902
906
|
</div>
|
|
903
907
|
```
|
|
@@ -1027,7 +1031,7 @@ Rules:
|
|
|
1027
1031
|
- **Do not set a fixed height on this component when used inside `highlight-cols`.** Let the parent grid's `align-items:stretch` control the column height. Fixed heights fight against the stretch and create misaligned baselines.
|
|
1028
1032
|
- **Image aspect ratio.** Aim for 16:9 or 3:2 crops for the image block. Portrait crops create tall image zones that push text down and unbalance the composition.
|
|
1029
1033
|
- **Kicker icon size.** Keep Lucide SVG icons at 16–20px. Larger icons shift visual weight from the image to the label zone.
|
|
1030
|
-
- **`editorial-list`
|
|
1034
|
+
- **`editorial-list` sizing.** Reuse the foundation list size. If a narrow column needs more room, shorten the copy before introducing a component-specific font override.
|
|
1031
1035
|
<!-- @component:editorial-image-top:end -->
|
|
1032
1036
|
|
|
1033
1037
|
<!-- @component:editorial-text-top:start -->
|
|
@@ -1093,7 +1097,7 @@ Rules:
|
|
|
1093
1097
|
##### Tips
|
|
1094
1098
|
- **Same height/stretch rule as `editorial-image-top`.** Do not set fixed heights; let parent grid stretch control the column.
|
|
1095
1099
|
- **When used as a center spine in `highlight-cols`,** this is the one component that may legitimately be taller than its neighbors. That density imbalance is intentional — do not try to equalize it with padding or extra content in the outer columns.
|
|
1096
|
-
- **`editorial-list`
|
|
1100
|
+
- **`editorial-list` sizing.** Reuse the foundation list size. If a narrow column needs more room, shorten the copy before introducing a component-specific font override.
|
|
1097
1101
|
<!-- @component:editorial-text-top:end -->
|
|
1098
1102
|
|
|
1099
1103
|
<!-- @component:editorial-text-left:start -->
|
|
@@ -1111,7 +1115,7 @@ Structure:
|
|
|
1111
1115
|
|
|
1112
1116
|
<!-- header: module title spans full width -->
|
|
1113
1117
|
<div class="editorial-text-left-header">
|
|
1114
|
-
<h3 style="
|
|
1118
|
+
<h3 style="line-height:1.08;">Module title — a single standalone heading above both columns</h3>
|
|
1115
1119
|
</div>
|
|
1116
1120
|
|
|
1117
1121
|
<div class="editorial-text-left-content">
|
|
@@ -1125,9 +1129,9 @@ Structure:
|
|
|
1125
1129
|
<!-- text-panel-body: place <p>, <ul class="editorial-list">, or both — choose based on content -->
|
|
1126
1130
|
<div class="text-panel-body" style="margin-top:12px;">
|
|
1127
1131
|
<!-- prose variant -->
|
|
1128
|
-
<p
|
|
1132
|
+
<p>Supporting description. One or two sentences that position this card within the broader page argument.</p>
|
|
1129
1133
|
<!-- bullet variant (use instead of or after prose): -->
|
|
1130
|
-
<!-- <ul class="editorial-list"
|
|
1134
|
+
<!-- <ul class="editorial-list">
|
|
1131
1135
|
<li><strong>Lead phrase.</strong> Supporting explanation for this point.</li>
|
|
1132
1136
|
<li><strong>Second point.</strong> One sentence of context or evidence.</li>
|
|
1133
1137
|
<li><strong>Third point.</strong> Keep each item roughly equal in length.</li>
|
|
@@ -1204,7 +1208,7 @@ Rules:
|
|
|
1204
1208
|
##### Tips
|
|
1205
1209
|
- **Parent must supply height.** `.editorial-text-left` uses `height: 100%` and `flex: 1` internally. The parent slot must have a defined height (grid cell, `height:100%` chain, or `flex:1;min-height:0`).
|
|
1206
1210
|
- **Text-to-visual flex ratio.** Default is `1.1 : 1` (copy slightly wider). For more copy, try `1.3 : 1`. For a visually dominant right panel, try `1 : 1.2`. Do not go below `0.8` on the copy side.
|
|
1207
|
-
- **`editorial-list`
|
|
1211
|
+
- **`editorial-list` inside copy zone.** Keep the foundation size. If the column feels cramped, reduce the amount of copy or widen the text side instead of introducing a smaller local text scale.
|
|
1208
1212
|
- **`echart-container` in visual slot.** Set `width:100%;height:100%` on the container and call `echarts.init()` after `SlidePresentation` is instantiated. The `position:relative;overflow:hidden` on `.editorial-text-left-visual` contains the canvas correctly.
|
|
1209
1213
|
- **`image-title` in visual slot.** The component is self-contained and fills `width:100%;height:100%` automatically. Use `image-title--right` modifier with a bottom-heavy overlay and right-biased blur mask for the most common editorial orientation.
|
|
1210
1214
|
- **Dark background.** Override CSS variables on `.editorial-text-left` to cascade into both the copy and visual zones: `--text-primary`, `--text-secondary`, `--text-muted`, `--line`, `--line-strong` — all set to white-family values.
|
|
@@ -1256,7 +1260,7 @@ chart.setOption({ /* LLM selects type and config */ });
|
|
|
1256
1260
|
|
|
1257
1261
|
.chart-subtitle {
|
|
1258
1262
|
margin-top: 4px;
|
|
1259
|
-
font-size:
|
|
1263
|
+
font-size: var(--font-size-body);
|
|
1260
1264
|
color: var(--text-muted);
|
|
1261
1265
|
line-height: 1.4;
|
|
1262
1266
|
}
|
|
@@ -1269,7 +1273,7 @@ chart.setOption({ /* LLM selects type and config */ });
|
|
|
1269
1273
|
.chart-caption {
|
|
1270
1274
|
flex-shrink: 0;
|
|
1271
1275
|
margin-top: 12px;
|
|
1272
|
-
font-size:
|
|
1276
|
+
font-size: var(--font-size-meta);
|
|
1273
1277
|
letter-spacing: 0.12em;
|
|
1274
1278
|
text-transform: uppercase;
|
|
1275
1279
|
color: var(--text-muted);
|
|
@@ -1326,13 +1330,13 @@ Horizontal step or phase sequence. Use for process stages, numbered definitions,
|
|
|
1326
1330
|
/* Shared by flow-horizontal and flow-vertical */
|
|
1327
1331
|
.flow-number {
|
|
1328
1332
|
font-family: var(--font-display);
|
|
1329
|
-
font-size:
|
|
1333
|
+
font-size: var(--font-size-meta);
|
|
1330
1334
|
font-weight: 700;
|
|
1331
1335
|
letter-spacing: 0.12em;
|
|
1332
1336
|
color: var(--text-muted);
|
|
1333
1337
|
border: 1px solid var(--line-strong);
|
|
1334
|
-
width:
|
|
1335
|
-
height:
|
|
1338
|
+
width: 40px;
|
|
1339
|
+
height: 40px;
|
|
1336
1340
|
display: flex;
|
|
1337
1341
|
align-items: center;
|
|
1338
1342
|
justify-content: center;
|
|
@@ -1346,7 +1350,6 @@ Horizontal step or phase sequence. Use for process stages, numbered definitions,
|
|
|
1346
1350
|
}
|
|
1347
1351
|
|
|
1348
1352
|
.flow-body p {
|
|
1349
|
-
font-size: 14px;
|
|
1350
1353
|
line-height: 1.6;
|
|
1351
1354
|
color: var(--text-secondary);
|
|
1352
1355
|
}
|
|
@@ -1558,7 +1561,7 @@ Annual-report format data table. Use for year-on-year comparisons, emissions dat
|
|
|
1558
1561
|
}
|
|
1559
1562
|
|
|
1560
1563
|
.data-table-label {
|
|
1561
|
-
font-size:
|
|
1564
|
+
font-size: var(--font-size-meta);
|
|
1562
1565
|
font-weight: 700;
|
|
1563
1566
|
letter-spacing: 0.14em;
|
|
1564
1567
|
text-transform: uppercase;
|
|
@@ -1570,7 +1573,7 @@ Annual-report format data table. Use for year-on-year comparisons, emissions dat
|
|
|
1570
1573
|
width: 100%;
|
|
1571
1574
|
border-collapse: collapse;
|
|
1572
1575
|
font-family: var(--font-body);
|
|
1573
|
-
font-size:
|
|
1576
|
+
font-size: var(--font-size-body);
|
|
1574
1577
|
font-variant-numeric: tabular-nums;
|
|
1575
1578
|
color: var(--text-primary);
|
|
1576
1579
|
}
|
|
@@ -1582,7 +1585,7 @@ Annual-report format data table. Use for year-on-year comparisons, emissions dat
|
|
|
1582
1585
|
.data-table th {
|
|
1583
1586
|
padding: 0 12px 10px 0;
|
|
1584
1587
|
text-align: left;
|
|
1585
|
-
font-size:
|
|
1588
|
+
font-size: var(--font-size-meta);
|
|
1586
1589
|
font-weight: 600;
|
|
1587
1590
|
letter-spacing: 0.1em;
|
|
1588
1591
|
text-transform: uppercase;
|
|
@@ -1629,7 +1632,7 @@ Annual-report format data table. Use for year-on-year comparisons, emissions dat
|
|
|
1629
1632
|
}
|
|
1630
1633
|
|
|
1631
1634
|
.data-table tr.section-header td {
|
|
1632
|
-
font-size:
|
|
1635
|
+
font-size: var(--font-size-meta);
|
|
1633
1636
|
font-weight: 700;
|
|
1634
1637
|
letter-spacing: 0.10em;
|
|
1635
1638
|
text-transform: uppercase;
|
|
@@ -1658,7 +1661,7 @@ Annual-report format data table. Use for year-on-year comparisons, emissions dat
|
|
|
1658
1661
|
|
|
1659
1662
|
.table-caption {
|
|
1660
1663
|
margin-top: 12px;
|
|
1661
|
-
font-size:
|
|
1664
|
+
font-size: var(--font-size-meta);
|
|
1662
1665
|
letter-spacing: 0.1em;
|
|
1663
1666
|
text-transform: uppercase;
|
|
1664
1667
|
color: var(--text-muted);
|
|
@@ -1677,7 +1680,7 @@ Rules:
|
|
|
1677
1680
|
- Include `.table-caption` below with the data source and unit.
|
|
1678
1681
|
|
|
1679
1682
|
##### Tips
|
|
1680
|
-
- **
|
|
1683
|
+
- **Dense tables.** Keep the table on the foundation text scale when possible. If the dataset is too large, simplify the table, split it into multiple tables, or reduce the number of columns before introducing a smaller local text size.
|
|
1681
1684
|
- **Dark background: override CSS variables on `.data-table-wrap`.** Set `--text-primary:#f7f4ee`, `--text-secondary:rgba(247,244,238,0.7)`, `--text-muted:rgba(247,244,238,0.45)`, `--line:rgba(247,244,238,0.12)`, `--line-strong:rgba(247,244,238,0.28)`. All child elements inherit automatically via `var()`.
|
|
1682
1685
|
- **`col-highlight` on dark.** Override background on `.data-table-wrap`: `.data-table th.col-highlight, .data-table td.col-highlight { background: rgba(247,244,238,0.06); }`. Also override `--accent-earth` → `var(--accent-gold)` so highlight header color remains visible.
|
|
1683
1686
|
- **Delta positive on dark.** Override `--accent-olive` → `#8faf7e` on `.data-table-wrap`. The default `--accent-olive` (#6f7562) is nearly invisible on dark backgrounds.
|
|
@@ -1807,7 +1810,7 @@ The LLM controls three key variables via inline style or modifier class:
|
|
|
1807
1810
|
|
|
1808
1811
|
/* Text elements */
|
|
1809
1812
|
.image-title-eyebrow {
|
|
1810
|
-
font-size:
|
|
1813
|
+
font-size: var(--font-size-meta);
|
|
1811
1814
|
font-weight: 700;
|
|
1812
1815
|
letter-spacing: 0.18em;
|
|
1813
1816
|
text-transform: uppercase;
|
|
@@ -1825,7 +1828,7 @@ The LLM controls three key variables via inline style or modifier class:
|
|
|
1825
1828
|
|
|
1826
1829
|
.image-title-subtitle {
|
|
1827
1830
|
margin-top: 24px;
|
|
1828
|
-
font-size:
|
|
1831
|
+
font-size: var(--font-size-body);
|
|
1829
1832
|
line-height: 1.56;
|
|
1830
1833
|
color: rgba(247, 244, 238, 0.72);
|
|
1831
1834
|
max-width: 480px;
|
|
@@ -1868,21 +1871,21 @@ Narrow editorial panel for table-of-contents slides. A 3px accent-gold vertical
|
|
|
1868
1871
|
<div style="padding-left:22px;display:flex;flex-direction:column;justify-content:space-between;flex:1;">
|
|
1869
1872
|
<div>
|
|
1870
1873
|
<h2 style="font-size:34px;line-height:0.94;letter-spacing:-0.03em;text-transform:uppercase;max-width:220px;">Table of Contents</h2>
|
|
1871
|
-
<p style="margin-top:18px;
|
|
1874
|
+
<p style="margin-top:18px;letter-spacing:0.04em;color:var(--text-secondary);max-width:255px;">Short introductory note describing the scope of the sections that follow.</p>
|
|
1872
1875
|
<ol style="list-style:none;display:flex;flex-direction:column;gap:10px;margin-top:26px;">
|
|
1873
|
-
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;
|
|
1874
|
-
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;
|
|
1875
|
-
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;
|
|
1876
|
-
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;
|
|
1877
|
-
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;
|
|
1878
|
-
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;
|
|
1876
|
+
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;border-bottom:1px solid var(--line);padding-bottom:8px;"><span style="font-weight:700;">01</span><span>Chapter title or section theme</span></li>
|
|
1877
|
+
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;border-bottom:1px solid var(--line);padding-bottom:8px;"><span style="font-weight:700;">02</span><span>Chapter title or section theme</span></li>
|
|
1878
|
+
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;border-bottom:1px solid var(--line);padding-bottom:8px;"><span style="font-weight:700;">03</span><span>Chapter title or section theme</span></li>
|
|
1879
|
+
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;border-bottom:1px solid var(--line);padding-bottom:8px;"><span style="font-weight:700;">04</span><span>Chapter title or section theme</span></li>
|
|
1880
|
+
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;border-bottom:1px solid var(--line);padding-bottom:8px;"><span style="font-weight:700;">05</span><span>Chapter title or section theme</span></li>
|
|
1881
|
+
<li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;"><span style="font-weight:700;">06</span><span>Chapter title or section theme</span></li>
|
|
1879
1882
|
</ol>
|
|
1880
1883
|
</div>
|
|
1881
1884
|
<div style="display:flex;flex-direction:column;gap:14px;">
|
|
1882
1885
|
<div class="rule"></div>
|
|
1883
1886
|
<div>
|
|
1884
1887
|
<p class="caption">Scope of report</p>
|
|
1885
|
-
<p style="margin-top:10px;
|
|
1888
|
+
<p style="margin-top:10px;color:var(--text-secondary);max-width:255px;">Optional scope note, data coverage period, or brief methodology reference.</p>
|
|
1886
1889
|
</div>
|
|
1887
1890
|
<div style="display:flex;justify-content:space-between;align-items:end;">
|
|
1888
1891
|
<p class="caption">Organisation · Year</p>
|
|
@@ -1975,7 +1978,7 @@ Flat editorial quote block. Wide and short (width > height). Transparent backgro
|
|
|
1975
1978
|
align-items: center;
|
|
1976
1979
|
justify-content: center;
|
|
1977
1980
|
font-family: var(--font-display);
|
|
1978
|
-
font-size:
|
|
1981
|
+
font-size: var(--font-size-body);
|
|
1979
1982
|
font-weight: 700;
|
|
1980
1983
|
color: var(--text-muted);
|
|
1981
1984
|
flex-shrink: 0;
|
|
@@ -1995,7 +1998,7 @@ Flat editorial quote block. Wide and short (width > height). Transparent backgro
|
|
|
1995
1998
|
}
|
|
1996
1999
|
|
|
1997
2000
|
.quote-name {
|
|
1998
|
-
font-size:
|
|
2001
|
+
font-size: var(--font-size-body);
|
|
1999
2002
|
font-weight: 600;
|
|
2000
2003
|
color: var(--text-primary);
|
|
2001
2004
|
line-height: 1.3;
|
|
@@ -2083,7 +2086,7 @@ Omit `--light` only on slides with a white/light background.
|
|
|
2083
2086
|
bottom: 36px;
|
|
2084
2087
|
right: 52px;
|
|
2085
2088
|
font-family: var(--font-display);
|
|
2086
|
-
font-size:
|
|
2089
|
+
font-size: var(--font-size-meta);
|
|
2087
2090
|
font-weight: 700;
|
|
2088
2091
|
letter-spacing: 0.18em;
|
|
2089
2092
|
color: var(--text-muted);
|
|
@@ -2217,7 +2220,7 @@ A horizontal milestone timeline with a central axis line. Nodes sit on the axis;
|
|
|
2217
2220
|
/* Date: inherits node colour via --tjh-item-color */
|
|
2218
2221
|
.tjh-date {
|
|
2219
2222
|
font-family: var(--font-display);
|
|
2220
|
-
font-size:
|
|
2223
|
+
font-size: var(--font-size-meta);
|
|
2221
2224
|
font-weight: 700;
|
|
2222
2225
|
letter-spacing: 0.16em;
|
|
2223
2226
|
text-transform: uppercase;
|
|
@@ -2228,7 +2231,7 @@ A horizontal milestone timeline with a central axis line. Nodes sit on the axis;
|
|
|
2228
2231
|
|
|
2229
2232
|
.tjh-title {
|
|
2230
2233
|
font-family: var(--font-display);
|
|
2231
|
-
font-size:
|
|
2234
|
+
font-size: var(--font-size-body);
|
|
2232
2235
|
font-weight: 600;
|
|
2233
2236
|
letter-spacing: -0.01em;
|
|
2234
2237
|
color: var(--text-primary);
|
|
@@ -2236,7 +2239,7 @@ A horizontal milestone timeline with a central axis line. Nodes sit on the axis;
|
|
|
2236
2239
|
}
|
|
2237
2240
|
|
|
2238
2241
|
.tjh-text {
|
|
2239
|
-
font-size:
|
|
2242
|
+
font-size: var(--font-size-body);
|
|
2240
2243
|
line-height: 1.5;
|
|
2241
2244
|
color: var(--text-secondary);
|
|
2242
2245
|
}
|
|
@@ -2412,7 +2415,7 @@ Can be placed inside any layout slot that provides a defined height (`narrative`
|
|
|
2412
2415
|
/* Date — colored per node via --tjv-item-color */
|
|
2413
2416
|
.tjv-date {
|
|
2414
2417
|
font-family: var(--font-display);
|
|
2415
|
-
font-size:
|
|
2418
|
+
font-size: var(--font-size-meta);
|
|
2416
2419
|
font-weight: 700;
|
|
2417
2420
|
letter-spacing: 0.16em;
|
|
2418
2421
|
text-transform: uppercase;
|
|
@@ -2431,7 +2434,7 @@ Can be placed inside any layout slot that provides a defined height (`narrative`
|
|
|
2431
2434
|
}
|
|
2432
2435
|
|
|
2433
2436
|
.tjv-text {
|
|
2434
|
-
font-size:
|
|
2437
|
+
font-size: var(--font-size-body);
|
|
2435
2438
|
line-height: 1.5;
|
|
2436
2439
|
color: var(--text-secondary);
|
|
2437
2440
|
max-width: 380px;
|
|
@@ -2453,7 +2456,7 @@ Rules:
|
|
|
2453
2456
|
- **Standalone full-page use**: set an explicit height on the `.tjv` wrapper (e.g. `height: 720px`) when used outside a height-constrained layout.
|
|
2454
2457
|
- **Dark background overrides**: set on the `.tjv` wrapper — `--line-strong: rgba(247,244,238,0.25)` (axis + stem), `.tjv-title { color: #f7f4ee }`, `.tjv-text { color: rgba(247,244,238,0.7) }`. The `--tjv-item-color` accent colours work on dark backgrounds without change.
|
|
2455
2458
|
- **Fewer nodes (3–4)**: increase spacing — use `top` values like `15%, 35%, 55%, 75%` to prevent the timeline from clustering at the top.
|
|
2456
|
-
- **More nodes (6–8)**:
|
|
2459
|
+
- **More nodes (6–8)**: keep `.tjv-text` to 1–2 lines and increase vertical spacing before shrinking the text scale.
|
|
2457
2460
|
<!-- @component:timeline-journey-vertical:end -->
|
|
2458
2461
|
|
|
2459
2462
|
<!-- @design:components:end -->
|
|
@@ -40,7 +40,7 @@ files in the workspace (PDF, Word, Excel, PowerPoint, CSV, text).
|
|
|
40
40
|
Then select the files relevant to your research axis.
|
|
41
41
|
|
|
42
42
|
For every selected file, call **\`revela-extract-document-materials\`** first.
|
|
43
|
-
- \`pptx\`, \`docx\`, and \`xlsx\` will produce a manifest plus extracted text and any available embedded materials
|
|
43
|
+
- \`pdf\`, \`pptx\`, \`docx\`, and \`xlsx\` will produce a manifest plus extracted text and any available embedded materials
|
|
44
44
|
- unsupported file types will be skipped automatically
|
|
45
45
|
|
|
46
46
|
After that, use the \`read\` tool on:
|
|
@@ -3,7 +3,10 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileS
|
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "path"
|
|
4
4
|
import { DOMParser } from "@xmldom/xmldom"
|
|
5
5
|
import { unzipSync } from "fflate"
|
|
6
|
+
import { Jimp } from "jimp"
|
|
7
|
+
import { extractImages, getDocumentProxy } from "unpdf"
|
|
6
8
|
import { extractDocx } from "../read-hooks/extractors/docx"
|
|
9
|
+
import { extractPdfText } from "../read-hooks/extractors/pdf"
|
|
7
10
|
import { extractPptx } from "../read-hooks/extractors/pptx"
|
|
8
11
|
import { extractXlsx } from "../read-hooks/extractors/xlsx"
|
|
9
12
|
|
|
@@ -48,7 +51,7 @@ export type PptxSlide = {
|
|
|
48
51
|
export type DocumentMaterialsResult = {
|
|
49
52
|
status: "processed" | "skipped" | "failed"
|
|
50
53
|
source: string
|
|
51
|
-
type: "pptx" | "docx" | "xlsx" | "other"
|
|
54
|
+
type: "pptx" | "docx" | "xlsx" | "pdf" | "other"
|
|
52
55
|
cache_dir?: string
|
|
53
56
|
manifest_path?: string
|
|
54
57
|
text_path?: string
|
|
@@ -83,8 +86,11 @@ const SUPPORTED_EXTENSIONS: Record<string, SupportedType> = {
|
|
|
83
86
|
".pptx": "pptx",
|
|
84
87
|
".docx": "docx",
|
|
85
88
|
".xlsx": "xlsx",
|
|
89
|
+
".pdf": "pdf",
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
type PdfImageData = Awaited<ReturnType<typeof extractImages>>[number]
|
|
93
|
+
|
|
88
94
|
function normalizeZipTarget(basePath: string, target: string): string {
|
|
89
95
|
const segments = join(dirname(basePath), target).split("/")
|
|
90
96
|
const normalized: string[] = []
|
|
@@ -151,6 +157,47 @@ function materialPath(cacheDir: string, workspaceDir: string, ...segments: strin
|
|
|
151
157
|
return workspaceRelative(join(cacheDir, ...segments), workspaceDir)
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
function toRgbaBuffer(image: PdfImageData): Buffer {
|
|
161
|
+
const pixelCount = image.width * image.height
|
|
162
|
+
|
|
163
|
+
if (image.channels === 4) {
|
|
164
|
+
return Buffer.from(image.data.buffer, image.data.byteOffset, image.data.byteLength)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const rgba = Buffer.alloc(pixelCount * 4)
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < pixelCount; i++) {
|
|
170
|
+
const dest = i * 4
|
|
171
|
+
if (image.channels === 3) {
|
|
172
|
+
const src = i * 3
|
|
173
|
+
rgba[dest] = image.data[src]!
|
|
174
|
+
rgba[dest + 1] = image.data[src + 1]!
|
|
175
|
+
rgba[dest + 2] = image.data[src + 2]!
|
|
176
|
+
rgba[dest + 3] = 255
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const value = image.data[i]!
|
|
181
|
+
rgba[dest] = value
|
|
182
|
+
rgba[dest + 1] = value
|
|
183
|
+
rgba[dest + 2] = value
|
|
184
|
+
rgba[dest + 3] = 255
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return rgba
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function encodePdfImageAsPng(image: PdfImageData): Promise<Buffer> {
|
|
191
|
+
const bitmap = {
|
|
192
|
+
data: toRgbaBuffer(image),
|
|
193
|
+
width: image.width,
|
|
194
|
+
height: image.height,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const png = Jimp.fromBitmap(bitmap)
|
|
198
|
+
return await png.getBuffer("image/png")
|
|
199
|
+
}
|
|
200
|
+
|
|
154
201
|
function parseXml(files: Record<string, Uint8Array>, path: string): any | null {
|
|
155
202
|
const file = files[path]
|
|
156
203
|
if (!file) return null
|
|
@@ -589,6 +636,94 @@ function extractTables(type: SupportedType, textPath: string): DocumentMaterial[
|
|
|
589
636
|
return [{ path: textPath, source_ref: "workbook", note: "Sheet text and tables extracted to text file" }]
|
|
590
637
|
}
|
|
591
638
|
|
|
639
|
+
async function extractPdfImages(buf: Buffer, cacheDir: string, workspaceDir: string): Promise<DocumentMaterial[]> {
|
|
640
|
+
const pdf = await getDocumentProxy(new Uint8Array(buf))
|
|
641
|
+
const images: DocumentMaterial[] = []
|
|
642
|
+
|
|
643
|
+
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
|
|
644
|
+
const extracted = await extractImages(pdf, pageNumber)
|
|
645
|
+
|
|
646
|
+
for (let index = 0; index < extracted.length; index++) {
|
|
647
|
+
const image = extracted[index]!
|
|
648
|
+
const exportedName = `page-${String(pageNumber).padStart(2, "0")}-image-${String(index + 1).padStart(2, "0")}.png`
|
|
649
|
+
const outputPath = join(cacheDir, "images", exportedName)
|
|
650
|
+
const png = await encodePdfImageAsPng(image)
|
|
651
|
+
writeFileSync(outputPath, new Uint8Array(png))
|
|
652
|
+
|
|
653
|
+
images.push({
|
|
654
|
+
path: materialPath(cacheDir, workspaceDir, "images", exportedName),
|
|
655
|
+
source_ref: `pdf/page-${String(pageNumber).padStart(2, "0")}/${image.key}`,
|
|
656
|
+
page_or_slide: `page-${String(pageNumber).padStart(2, "0")}`,
|
|
657
|
+
note: `Embedded PDF image (${image.width}x${image.height}, ${image.channels} channel${image.channels === 1 ? "" : "s"})`,
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return images
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function processPdfFile(filePath: string, workspaceDir: string): Promise<DocumentMaterialsResult> {
|
|
666
|
+
const relativeSource = workspaceRelative(filePath, workspaceDir)
|
|
667
|
+
const fingerprint = buildFingerprint(filePath)
|
|
668
|
+
const cacheDir = join(workspaceDir, ".opencode", "revela", "doc-materials", fingerprint)
|
|
669
|
+
const manifestPath = join(cacheDir, "manifest.json")
|
|
670
|
+
|
|
671
|
+
if (existsSync(manifestPath)) {
|
|
672
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")) as CachedManifest
|
|
673
|
+
return {
|
|
674
|
+
status: "processed",
|
|
675
|
+
source: manifest.source,
|
|
676
|
+
type: manifest.type,
|
|
677
|
+
cache_dir: manifest.cache_dir,
|
|
678
|
+
manifest_path: manifest.manifest_path,
|
|
679
|
+
text_path: manifest.text_path,
|
|
680
|
+
images: manifest.images,
|
|
681
|
+
skipped_assets: manifest.skipped_assets,
|
|
682
|
+
slides: manifest.slides,
|
|
683
|
+
tables: manifest.tables,
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
mkdirSync(join(cacheDir, "images"), { recursive: true })
|
|
688
|
+
mkdirSync(join(cacheDir, "tables"), { recursive: true })
|
|
689
|
+
|
|
690
|
+
const buf = readFileSync(filePath)
|
|
691
|
+
const text = await extractPdfText(buf)
|
|
692
|
+
const textPath = join(cacheDir, "text.txt")
|
|
693
|
+
writeFileSync(textPath, `[Extracted from: ${basename(filePath)}]\n\n${text}`, "utf-8")
|
|
694
|
+
|
|
695
|
+
const images = await extractPdfImages(buf, cacheDir, workspaceDir)
|
|
696
|
+
|
|
697
|
+
const result: DocumentMaterialsResult = {
|
|
698
|
+
status: "processed",
|
|
699
|
+
source: relativeSource,
|
|
700
|
+
type: "pdf",
|
|
701
|
+
cache_dir: workspaceRelative(cacheDir, workspaceDir),
|
|
702
|
+
manifest_path: workspaceRelative(manifestPath, workspaceDir),
|
|
703
|
+
text_path: workspaceRelative(textPath, workspaceDir),
|
|
704
|
+
images,
|
|
705
|
+
skipped_assets: [],
|
|
706
|
+
slides: [],
|
|
707
|
+
tables: [],
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const manifest: CachedManifest = {
|
|
711
|
+
source: result.source,
|
|
712
|
+
type: "pdf",
|
|
713
|
+
fingerprint,
|
|
714
|
+
cache_dir: result.cache_dir!,
|
|
715
|
+
manifest_path: result.manifest_path!,
|
|
716
|
+
text_path: result.text_path!,
|
|
717
|
+
images: result.images ?? [],
|
|
718
|
+
skipped_assets: [],
|
|
719
|
+
slides: [],
|
|
720
|
+
tables: [],
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8")
|
|
724
|
+
return result
|
|
725
|
+
}
|
|
726
|
+
|
|
592
727
|
async function processOfficeFile(filePath: string, workspaceDir: string, type: SupportedType): Promise<DocumentMaterialsResult> {
|
|
593
728
|
const relativeSource = workspaceRelative(filePath, workspaceDir)
|
|
594
729
|
const fingerprint = buildFingerprint(filePath)
|
|
@@ -683,7 +818,9 @@ export async function extractDocumentMaterials(filePath: string, workspaceDir: s
|
|
|
683
818
|
}
|
|
684
819
|
}
|
|
685
820
|
|
|
686
|
-
return
|
|
821
|
+
return type === "pdf"
|
|
822
|
+
? await processPdfFile(resolvedFile, workspaceDir)
|
|
823
|
+
: await processOfficeFile(resolvedFile, workspaceDir, type)
|
|
687
824
|
} catch (e) {
|
|
688
825
|
return {
|
|
689
826
|
status: "failed",
|