@cyberskill/design 1.5.1 → 1.6.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.
Files changed (110) hide show
  1. package/DESIGN.md +1 -1
  2. package/README.md +4 -4
  3. package/SKILL.md +3 -3
  4. package/VERSION +1 -1
  5. package/_ds_bundle.js +2 -2
  6. package/_esm/cs.mjs +2 -2
  7. package/docs/contrast-report.md +1 -1
  8. package/docs/conventions.md +4 -4
  9. package/docs/quality-gates.md +2 -2
  10. package/docs/release-notes.md +1 -1
  11. package/docs/vi/contrast-report.md +1 -1
  12. package/docs/vi/conventions.md +2 -2
  13. package/docs/vi/quality-gates.md +2 -2
  14. package/docs/vi/release-notes.md +1 -1
  15. package/llms.txt +3 -3
  16. package/package.json +5 -4
  17. package/templates/_vendor/doc-export.js +435 -0
  18. package/templates/app-shell/ds-base.js +8 -0
  19. package/templates/article/ds-base.js +8 -0
  20. package/templates/auth/ds-base.js +8 -0
  21. package/templates/bod-deck/ds-base.js +8 -0
  22. package/templates/bod-investor-update/ds-base.js +8 -0
  23. package/templates/bod-memo/ds-base.js +8 -0
  24. package/templates/bod-minutes/ds-base.js +8 -0
  25. package/templates/bod-report/ds-base.js +8 -0
  26. package/templates/bod-resolution/ds-base.js +8 -0
  27. package/templates/culture-event-invite/ds-base.js +8 -0
  28. package/templates/dashboard/ds-base.js +8 -0
  29. package/templates/delivery-change-note/ds-base.js +8 -0
  30. package/templates/delivery-kickoff/ds-base.js +8 -0
  31. package/templates/delivery-qbr-deck/ds-base.js +8 -0
  32. package/templates/delivery-runbook/ds-base.js +8 -0
  33. package/templates/delivery-status-email/ds-base.js +8 -0
  34. package/templates/doc-style-guide/ds-base.js +8 -0
  35. package/templates/doc-suite-index/ds-base.js +8 -0
  36. package/templates/doc-templates/ds-base.js +8 -0
  37. package/templates/email/ds-base.js +8 -0
  38. package/templates/finance-dunning-email/ds-base.js +8 -0
  39. package/templates/finance-expense-report/ds-base.js +8 -0
  40. package/templates/finance-invoice/ds-base.js +8 -0
  41. package/templates/finance-quote/ds-base.js +8 -0
  42. package/templates/hr-announcement/ds-base.js +8 -0
  43. package/templates/hr-interview-kit/ds-base.js +8 -0
  44. package/templates/hr-offboarding/ds-base.js +8 -0
  45. package/templates/hr-performance-review/ds-base.js +8 -0
  46. package/templates/hr-pip/ds-base.js +8 -0
  47. package/templates/legal-msa/ds-base.js +8 -0
  48. package/templates/legal-sow/ds-base.js +8 -0
  49. package/templates/marketing-campaign-brief/ds-base.js +8 -0
  50. package/templates/marketing-case-study/ds-base.js +8 -0
  51. package/templates/marketing-launch/ds-base.js +8 -0
  52. package/templates/marketing-newsletter/ds-base.js +8 -0
  53. package/templates/marketing-page/ds-base.js +8 -0
  54. package/templates/marketing-press-release/ds-base.js +8 -0
  55. package/templates/marketing-social-kit/ds-base.js +8 -0
  56. package/templates/sales-one-pager/ds-base.js +8 -0
  57. package/templates/sales-proposal-deck/ds-base.js +8 -0
  58. package/templates/settings/ds-base.js +8 -0
  59. package/templates/slide-deck/ds-base.js +8 -0
  60. package/templates/team-meeting-agenda/ds-base.js +8 -0
  61. package/templates/tech-incident-report/ds-base.js +8 -0
  62. package/templates/tech-release-notes/ds-base.js +8 -0
  63. package/templates/tech-retro/ds-base.js +8 -0
  64. package/templates/tech-rfc/ds-base.js +8 -0
  65. package/templates/vn-appointment-transfer/ds-base.js +8 -0
  66. package/templates/vn-certificate-of-employment/ds-base.js +8 -0
  67. package/templates/vn-code-of-conduct/ds-base.js +8 -0
  68. package/templates/vn-compensation-regulation/ds-base.js +8 -0
  69. package/templates/vn-contract-amendment/ds-base.js +8 -0
  70. package/templates/vn-contractor-agreement/ds-base.js +8 -0
  71. package/templates/vn-data-protection-policy/ds-base.js +8 -0
  72. package/templates/vn-disciplinary-case-file/ds-base.js +8 -0
  73. package/templates/vn-disciplinary-schedule/ds-base.js +8 -0
  74. package/templates/vn-employee-request/ds-base.js +8 -0
  75. package/templates/vn-exit-handover/ds-base.js +8 -0
  76. package/templates/vn-foreign-employee-pack/ds-base.js +8 -0
  77. package/templates/vn-framework-agreement/ds-base.js +8 -0
  78. package/templates/vn-grassroots-democracy/ds-base.js +8 -0
  79. package/templates/vn-infosec-policy/ds-base.js +8 -0
  80. package/templates/vn-internal-labor-regulations/ds-base.js +8 -0
  81. package/templates/vn-internship-agreement/ds-base.js +8 -0
  82. package/templates/vn-job-description/ds-base.js +8 -0
  83. package/templates/vn-labor-contract/ds-base.js +8 -0
  84. package/templates/vn-labour-management-book/ds-base.js +8 -0
  85. package/templates/vn-mutual-nda/ds-base.js +8 -0
  86. package/templates/vn-ndnca-ip/ds-base.js +8 -0
  87. package/templates/vn-offer-letter/ds-base.js +8 -0
  88. package/templates/vn-onboarding-checklist/ds-base.js +8 -0
  89. package/templates/vn-pdpl-consent/ds-base.js +8 -0
  90. package/templates/vn-performance-evaluation-regulation/ds-base.js +8 -0
  91. package/templates/vn-performance-review-form/ds-base.js +8 -0
  92. package/templates/vn-phantom-stock/ds-base.js +8 -0
  93. package/templates/vn-probation-contract/ds-base.js +8 -0
  94. package/templates/vn-receipt-acknowledgement/ds-base.js +8 -0
  95. package/templates/vn-remote-work-policy/ds-base.js +8 -0
  96. package/templates/vn-resignation-letter/ds-base.js +8 -0
  97. package/templates/vn-salary-scale/ds-base.js +8 -0
  98. package/templates/vn-sow-appendix/ds-base.js +8 -0
  99. package/templates/vn-termination-decision/ds-base.js +8 -0
  100. package/templates/vn-total-rewards-appendix/ds-base.js +8 -0
  101. package/templates/vn-training-commitment/ds-base.js +8 -0
  102. package/templates/vn-travel-expense-policy/ds-base.js +8 -0
  103. package/tokens/element-seeds.json +1 -1
  104. package/tokens/native/CSTokens.kt +1 -1
  105. package/tokens/native/CSTokens.swift +1 -1
  106. package/tokens/native/cs_tokens.dart +1 -1
  107. package/tokens/provenance.json +6 -6
  108. package/tokens/tokens.dtcg.json +1 -1
  109. package/tokens/tokens.js +2 -2
  110. package/tokens/tokens.json +1 -1
@@ -0,0 +1,435 @@
1
+ /**
2
+ * CyberSkill document export — PDF (print) + editable OOXML DOCX.
3
+ * Loaded by ds-base.js when <meta name="omelette-owns-print"> is present.
4
+ * No CDN; ZIP via CompressionStream (deflate-raw).
5
+ */
6
+ (() => {
7
+ const UMBER = '45210E';
8
+ const A4 = { w: 11906, h: 16838, name: 'A4' }; // twips
9
+ const LETTER = { w: 12240, h: 15840, name: 'Letter' };
10
+
11
+ function pageSizeFromCss() {
12
+ const styles = [...document.querySelectorAll('style')].map((s) => s.textContent || '').join('\n');
13
+ if (/@page\s*\{[^}]*size:\s*letter/i.test(styles)) return LETTER;
14
+ return A4;
15
+ }
16
+
17
+ function xmlEscape(s) {
18
+ return String(s)
19
+ .replace(/&/g, '&amp;')
20
+ .replace(/</g, '&lt;')
21
+ .replace(/>/g, '&gt;')
22
+ .replace(/"/g, '&quot;');
23
+ }
24
+
25
+ function crc32(buf) {
26
+ let c = ~0;
27
+ for (let i = 0; i < buf.length; i++) {
28
+ c ^= buf[i];
29
+ for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xedb88320 & -(c & 1));
30
+ }
31
+ return ~c >>> 0;
32
+ }
33
+
34
+ async function deflateRaw(u8) {
35
+ if (typeof CompressionStream === 'undefined') {
36
+ // Store uncompressed (ZIP method 0) — still a valid OOXML package.
37
+ return { data: u8, method: 0 };
38
+ }
39
+ const cs = new CompressionStream('deflate-raw');
40
+ const writer = cs.writable.getWriter();
41
+ writer.write(u8);
42
+ writer.close();
43
+ const ab = await new Response(cs.readable).arrayBuffer();
44
+ return { data: new Uint8Array(ab), method: 8 };
45
+ }
46
+
47
+ async function buildZip(files) {
48
+ // files: [{ name, bytes: Uint8Array }]
49
+ const enc = new TextEncoder();
50
+ const locals = [];
51
+ const centrals = [];
52
+ let offset = 0;
53
+ for (const f of files) {
54
+ const nameBytes = enc.encode(f.name);
55
+ const { data, method } = await deflateRaw(f.bytes);
56
+ const crc = crc32(f.bytes);
57
+ const local = new Uint8Array(30 + nameBytes.length + data.length);
58
+ const dv = new DataView(local.buffer);
59
+ dv.setUint32(0, 0x04034b50, true);
60
+ dv.setUint16(4, 20, true);
61
+ dv.setUint16(6, 0, true);
62
+ dv.setUint16(8, method, true);
63
+ dv.setUint16(10, 0, true);
64
+ dv.setUint16(12, 0, true);
65
+ dv.setUint32(14, crc, true);
66
+ dv.setUint32(18, data.length, true);
67
+ dv.setUint32(22, f.bytes.length, true);
68
+ dv.setUint16(26, nameBytes.length, true);
69
+ dv.setUint16(28, 0, true);
70
+ local.set(nameBytes, 30);
71
+ local.set(data, 30 + nameBytes.length);
72
+ locals.push(local);
73
+
74
+ const central = new Uint8Array(46 + nameBytes.length);
75
+ const cv = new DataView(central.buffer);
76
+ cv.setUint32(0, 0x02014b50, true);
77
+ cv.setUint16(4, 20, true);
78
+ cv.setUint16(6, 20, true);
79
+ cv.setUint16(8, 0, true);
80
+ cv.setUint16(10, method, true);
81
+ cv.setUint16(12, 0, true);
82
+ cv.setUint16(14, 0, true);
83
+ cv.setUint32(16, crc, true);
84
+ cv.setUint32(20, data.length, true);
85
+ cv.setUint32(24, f.bytes.length, true);
86
+ cv.setUint16(28, nameBytes.length, true);
87
+ cv.setUint16(30, 0, true);
88
+ cv.setUint16(32, 0, true);
89
+ cv.setUint16(34, 0, true);
90
+ cv.setUint16(36, 0, true);
91
+ cv.setUint32(38, 0, true);
92
+ cv.setUint32(42, offset, true);
93
+ central.set(nameBytes, 46);
94
+ centrals.push(central);
95
+ offset += local.length;
96
+ }
97
+ const centralSize = centrals.reduce((n, b) => n + b.length, 0);
98
+ const end = new Uint8Array(22);
99
+ const ev = new DataView(end.buffer);
100
+ ev.setUint32(0, 0x06054b50, true);
101
+ ev.setUint16(4, 0, true);
102
+ ev.setUint16(6, 0, true);
103
+ ev.setUint16(8, files.length, true);
104
+ ev.setUint16(10, files.length, true);
105
+ ev.setUint32(12, centralSize, true);
106
+ ev.setUint32(16, offset, true);
107
+ ev.setUint16(20, 0, true);
108
+
109
+ const total =
110
+ locals.reduce((n, b) => n + b.length, 0) + centralSize + end.length;
111
+ const out = new Uint8Array(total);
112
+ let o = 0;
113
+ for (const b of locals) {
114
+ out.set(b, o);
115
+ o += b.length;
116
+ }
117
+ for (const b of centrals) {
118
+ out.set(b, o);
119
+ o += b.length;
120
+ }
121
+ out.set(end, o);
122
+ return out;
123
+ }
124
+
125
+ function textRuns(el) {
126
+ const parts = [];
127
+ const walk = (node, italic, bold, color) => {
128
+ if (node.nodeType === 3) {
129
+ const t = node.textContent;
130
+ if (t && t.replace(/\s+/g, '').length) parts.push({ t, italic, bold, color });
131
+ return;
132
+ }
133
+ if (node.nodeType !== 1) return;
134
+ if (node.getAttribute && node.getAttribute('data-omelette-chrome') != null) return;
135
+ const tag = node.tagName.toLowerCase();
136
+ if (tag === 'script' || tag === 'style' || tag === 'noscript') return;
137
+ const st = window.getComputedStyle(node);
138
+ const nextItalic =
139
+ italic || st.fontStyle === 'italic' || tag === 'i' || tag === 'em';
140
+ const nextBold =
141
+ bold ||
142
+ Number(st.fontWeight) >= 600 ||
143
+ tag === 'b' ||
144
+ tag === 'strong' ||
145
+ /^h[1-6]$/.test(tag);
146
+ let nextColor = color;
147
+ if (/^h[1-6]$/.test(tag) || (st.color && /rgb\(\s*69\s*,\s*33\s*,\s*14\s*\)/.test(st.color))) {
148
+ nextColor = UMBER;
149
+ }
150
+ for (const c of node.childNodes) walk(c, nextItalic, nextBold, nextColor);
151
+ };
152
+ walk(el, false, false, null);
153
+ return parts;
154
+ }
155
+
156
+ function runsToXml(runs) {
157
+ if (!runs.length) {
158
+ return '<w:r><w:t xml:space="preserve"></w:t></w:r>';
159
+ }
160
+ return runs
161
+ .map((r) => {
162
+ const rPr = [];
163
+ if (r.bold) rPr.push('<w:b/>');
164
+ if (r.italic) rPr.push('<w:i/>');
165
+ if (r.color) rPr.push(`<w:color w:val="${r.color}"/>`);
166
+ rPr.push('<w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:cs="Arial"/>');
167
+ const pr = rPr.length ? `<w:rPr>${rPr.join('')}</w:rPr>` : '';
168
+ return `<w:r>${pr}<w:t xml:space="preserve">${xmlEscape(r.t)}</w:t></w:r>`;
169
+ })
170
+ .join('');
171
+ }
172
+
173
+ function paraXml(el, style) {
174
+ const runs = textRuns(el);
175
+ if (!runs.length && !el.querySelector?.('br')) return '';
176
+ const pPr = style ? `<w:pPr><w:pStyle w:val="${style}"/></w:pPr>` : '';
177
+ return `<w:p>${pPr}${runsToXml(runs)}</w:p>`;
178
+ }
179
+
180
+ function tableXml(table) {
181
+ const rows = [...table.querySelectorAll(':scope > tbody > tr, :scope > thead > tr, :scope > tr')];
182
+ if (!rows.length) return '';
183
+ const gridCols = Math.max(...rows.map((r) => r.children.length), 1);
184
+ const colW = Math.floor(9000 / gridCols);
185
+ let xml = `<w:tbl><w:tblPr><w:tblW w:w="5000" w:type="pct"/><w:tblBorders>
186
+ <w:top w:val="single" w:sz="4" w:color="C9B8A8"/>
187
+ <w:left w:val="single" w:sz="4" w:color="C9B8A8"/>
188
+ <w:bottom w:val="single" w:sz="4" w:color="C9B8A8"/>
189
+ <w:right w:val="single" w:sz="4" w:color="C9B8A8"/>
190
+ <w:insideH w:val="single" w:sz="4" w:color="C9B8A8"/>
191
+ <w:insideV w:val="single" w:sz="4" w:color="C9B8A8"/>
192
+ </w:tblBorders></w:tblPr><w:tblGrid>${'<w:gridCol w:w="' + colW + '"/>'.repeat(gridCols)}</w:tblGrid>`;
193
+ for (const tr of rows) {
194
+ xml += '<w:tr>';
195
+ for (const cell of tr.children) {
196
+ const bg = window.getComputedStyle(cell).backgroundColor;
197
+ const shade =
198
+ bg && /rgb\(\s*69\s*,\s*33\s*,\s*14\s*\)/.test(bg)
199
+ ? `<w:tcPr><w:shd w:val="clear" w:fill="${UMBER}"/><w:tcW w:w="${colW}" w:type="dxa"/></w:tcPr>`
200
+ : `<w:tcPr><w:tcW w:w="${colW}" w:type="dxa"/></w:tcPr>`;
201
+ const cellColor = shade.includes(UMBER) ? 'FFFFFF' : null;
202
+ const runs = textRuns(cell).map((r) =>
203
+ cellColor ? { ...r, color: r.color || cellColor } : r,
204
+ );
205
+ xml += `<w:tc>${shade}<w:p>${runsToXml(runs)}</w:p></w:tc>`;
206
+ }
207
+ xml += '</w:tr>';
208
+ }
209
+ xml += '</w:tbl>';
210
+ return xml;
211
+ }
212
+
213
+ function sheetBodyXml(sheet) {
214
+ const blocks = [];
215
+ const kids = [...sheet.children];
216
+ const visit = (nodes) => {
217
+ for (const el of nodes) {
218
+ if (el.nodeType !== 1) continue;
219
+ if (el.getAttribute('data-omelette-chrome') != null) continue;
220
+ const tag = el.tagName.toLowerCase();
221
+ if (tag === 'table') {
222
+ blocks.push(tableXml(el));
223
+ continue;
224
+ }
225
+ if (/^h[1-6]$/.test(tag)) {
226
+ blocks.push(paraXml(el, 'Heading1'));
227
+ continue;
228
+ }
229
+ if (tag === 'p' || tag === 'div' || tag === 'section' || tag === 'li') {
230
+ // Prefer leaf paragraphs; if a div only wraps structure, descend.
231
+ const hasBlock = [...el.children].some((c) =>
232
+ /^(DIV|TABLE|SECTION|UL|OL|H1|H2|H3|H4|H5|H6)$/.test(c.tagName),
233
+ );
234
+ if (hasBlock && tag !== 'p') {
235
+ visit([...el.children]);
236
+ } else {
237
+ const px = paraXml(el);
238
+ if (px) blocks.push(px);
239
+ }
240
+ continue;
241
+ }
242
+ if (tag === 'ul' || tag === 'ol') {
243
+ visit([...el.children]);
244
+ continue;
245
+ }
246
+ visit([...el.children]);
247
+ }
248
+ };
249
+ visit(kids);
250
+ return blocks.filter(Boolean).join('');
251
+ }
252
+
253
+ function headerFooterText(sheet) {
254
+ const headerEl = sheet.querySelector(':scope > div');
255
+ const header = headerEl ? textRuns(headerEl).map((r) => r.t).join('').trim() : 'CyberSkill';
256
+ const footer = 'CyberSkill · MST 0316475200 · Hiện Thực Hoá Ý Chí';
257
+ return { header: header.slice(0, 200) || 'CyberSkill', footer };
258
+ }
259
+
260
+ async function buildDocxBlob(sheet) {
261
+ const page = pageSizeFromCss();
262
+ const enc = new TextEncoder();
263
+ const { header, footer } = headerFooterText(sheet);
264
+ const body = sheetBodyXml(sheet);
265
+
266
+ const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
267
+ <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
268
+ <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
269
+ <Default Extension="xml" ContentType="application/xml"/>
270
+ <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
271
+ <Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
272
+ <Override PartName="/word/header1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"/>
273
+ <Override PartName="/word/footer1.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>
274
+ </Types>`;
275
+
276
+ const rels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
277
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
278
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
279
+ </Relationships>`;
280
+
281
+ const docRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
282
+ <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
283
+ <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
284
+ <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" Target="header1.xml"/>
285
+ <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer1.xml"/>
286
+ </Relationships>`;
287
+
288
+ const styles = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
289
+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
290
+ <w:style w:type="paragraph" w:default="1" w:styleId="Normal">
291
+ <w:name w:val="Normal"/><w:qFormat/>
292
+ <w:rPr><w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:cs="Arial"/><w:sz w:val="22"/></w:rPr>
293
+ </w:style>
294
+ <w:style w:type="paragraph" w:styleId="Heading1">
295
+ <w:name w:val="heading 1"/><w:basedOn w:val="Normal"/><w:qFormat/>
296
+ <w:rPr><w:b/><w:color w:val="${UMBER}"/><w:sz w:val="28"/></w:rPr>
297
+ </w:style>
298
+ </w:styles>`;
299
+
300
+ const headerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
301
+ <w:hdr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
302
+ <w:p><w:pPr><w:pBdr><w:bottom w:val="single" w:sz="6" w:color="C9B8A8"/></w:pBdr></w:pPr>
303
+ <w:r><w:rPr><w:b/><w:color w:val="${UMBER}"/><w:sz w:val="18"/></w:rPr>
304
+ <w:t xml:space="preserve">${xmlEscape(header)}</w:t></w:r>
305
+ </w:p>
306
+ </w:hdr>`;
307
+
308
+ const footerXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
309
+ <w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
310
+ xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml">
311
+ <w:p>
312
+ <w:r><w:rPr><w:sz w:val="16"/><w:color w:val="6B5D4F"/></w:rPr>
313
+ <w:t xml:space="preserve">${xmlEscape(footer)} · </w:t></w:r>
314
+ <w:r><w:fldChar w:fldCharType="begin"/></w:r>
315
+ <w:r><w:instrText xml:space="preserve"> PAGE </w:instrText></w:r>
316
+ <w:r><w:fldChar w:fldCharType="end"/></w:r>
317
+ </w:p>
318
+ </w:ftr>`;
319
+
320
+ const documentXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
321
+ <w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
322
+ xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
323
+ <w:body>
324
+ ${body || '<w:p><w:r><w:t></w:t></w:r></w:p>'}
325
+ <w:sectPr>
326
+ <w:headerReference w:type="default" r:id="rId2"/>
327
+ <w:footerReference w:type="default" r:id="rId3"/>
328
+ <w:pgSz w:w="${page.w}" w:h="${page.h}"/>
329
+ <w:pgMar w:top="1440" w:right="1134" w:bottom="1134" w:left="1134" w:header="720" w:footer="720"/>
330
+ </w:sectPr>
331
+ </w:body>
332
+ </w:document>`;
333
+
334
+ const zip = await buildZip([
335
+ { name: '[Content_Types].xml', bytes: enc.encode(contentTypes) },
336
+ { name: '_rels/.rels', bytes: enc.encode(rels) },
337
+ { name: 'word/document.xml', bytes: enc.encode(documentXml) },
338
+ { name: 'word/_rels/document.xml.rels', bytes: enc.encode(docRels) },
339
+ { name: 'word/styles.xml', bytes: enc.encode(styles) },
340
+ { name: 'word/header1.xml', bytes: enc.encode(headerXml) },
341
+ { name: 'word/footer1.xml', bytes: enc.encode(footerXml) },
342
+ ]);
343
+ return new Blob([zip], {
344
+ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
345
+ });
346
+ }
347
+
348
+ function downloadBlob(blob, filename) {
349
+ const a = document.createElement('a');
350
+ a.href = URL.createObjectURL(blob);
351
+ a.download = filename;
352
+ a.rel = 'noopener';
353
+ document.body.appendChild(a);
354
+ a.click();
355
+ setTimeout(() => {
356
+ URL.revokeObjectURL(a.href);
357
+ a.remove();
358
+ }, 1500);
359
+ }
360
+
361
+ function suggestName(ext) {
362
+ const t = (document.title || 'document').replace(/\s*[·|].*$/, '').trim() || 'document';
363
+ return t.replace(/[^\w\u00C0-\u024F\- ]+/g, '').replace(/\s+/g, '-').slice(0, 80) + ext;
364
+ }
365
+
366
+ function injectToolbar() {
367
+ if (document.querySelector('[data-omelette-chrome="export"]')) return;
368
+ const meta = document.querySelector('meta[name="omelette-owns-print"]');
369
+ if (!meta) return;
370
+
371
+ const style = document.createElement('style');
372
+ style.setAttribute('data-omelette-chrome', 'export-style');
373
+ style.textContent = `
374
+ [data-omelette-chrome="export"]{
375
+ position:fixed;z-index:2147483000;top:12px;right:12px;display:flex;gap:8px;
376
+ font-family:var(--cs-font-family-ui,system-ui,sans-serif);pointer-events:auto;
377
+ }
378
+ [data-omelette-chrome="export"] button{
379
+ appearance:none;border:1px solid #c9b8a8;background:#fff;color:#45210E;
380
+ border-radius:8px;padding:8px 12px;font-size:12.5px;font-weight:700;cursor:pointer;
381
+ box-shadow:0 1px 2px rgba(35,26,18,.08);
382
+ }
383
+ [data-omelette-chrome="export"] button:hover{background:#f7f0e6}
384
+ [data-omelette-chrome="export"] button:focus-visible{outline:2px solid #F4BA17;outline-offset:2px}
385
+ @media print{
386
+ [data-omelette-chrome="export"],[data-omelette-chrome="export-style"]{display:none!important}
387
+ }
388
+ `;
389
+ document.head.appendChild(style);
390
+
391
+ const bar = document.createElement('div');
392
+ bar.setAttribute('data-omelette-chrome', 'export');
393
+ bar.setAttribute('role', 'toolbar');
394
+ bar.setAttribute('aria-label', 'Document export · Xuất tài liệu');
395
+
396
+ const pdfBtn = document.createElement('button');
397
+ pdfBtn.type = 'button';
398
+ pdfBtn.textContent = 'PDF · In / Print';
399
+ pdfBtn.addEventListener('click', () => window.print());
400
+
401
+ const docxBtn = document.createElement('button');
402
+ docxBtn.type = 'button';
403
+ docxBtn.textContent = 'DOCX · Tải / Download';
404
+ docxBtn.addEventListener('click', async () => {
405
+ try {
406
+ const sheet = document.querySelector('.cs-sheet') || document.body;
407
+ const blob = await buildDocxBlob(sheet);
408
+ downloadBlob(blob, suggestName('.docx'));
409
+ } catch (e) {
410
+ console.error('[doc-export] DOCX failed', e);
411
+ alert('DOCX export failed: ' + (e && e.message ? e.message : e));
412
+ }
413
+ });
414
+
415
+ bar.appendChild(pdfBtn);
416
+ bar.appendChild(docxBtn);
417
+ document.body.appendChild(bar);
418
+ }
419
+
420
+ // Test / automation hooks
421
+ window.__csDocExport = {
422
+ buildDocxBlob,
423
+ pageSizeFromCss,
424
+ injectToolbar,
425
+ };
426
+
427
+ const boot = () => injectToolbar();
428
+ if (document.readyState === 'loading') {
429
+ document.addEventListener('DOMContentLoaded', boot);
430
+ } else {
431
+ boot();
432
+ }
433
+ // DC boots async — re-try after paint so .cs-sheet exists for future exports.
434
+ setTimeout(boot, 800);
435
+ })();
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');
@@ -22,6 +22,14 @@
22
22
  s.onerror = () => console.error('ds-base.js: failed to load ' + s.src + ' — if this is a consuming project, point the base line in ds-base.js at the bound _ds/<folder> tree relative to this page (e.g. _ds/<folder> at the project root, ../_ds/<folder> one level down); in a fresh design system this can just mean the bundle is not compiled yet');
23
23
  document.head.appendChild(s);
24
24
 
25
+ // Print documents: PDF (window.print) + editable DOCX toolbar (TASK-IMP-022).
26
+ // Path is relative to this template folder → templates/_vendor/doc-export.js
27
+ if (document.querySelector('meta[name="omelette-owns-print"]')) {
28
+ const ex = document.createElement('script');
29
+ ex.src = '../_vendor/doc-export.js';
30
+ document.head.appendChild(ex);
31
+ }
32
+
25
33
  const syncLang = () => {
26
34
  try {
27
35
  const a = document.querySelector('[lang]');