collavre 0.14.0 → 0.15.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e56759c8d6833552a9d5e49b19745aca7d0a58d56ad3c3a7e6d502ea54d6b156
4
- data.tar.gz: 37a2481e6fc5abc1077167d481abb51b118f256de05c704cdd64381eb090562a
3
+ metadata.gz: 617bf6c7eda577396e21384c92f4655c44eb0f433ef2cfeba05f6c47ab00f2c9
4
+ data.tar.gz: b3cd87f98abf7a7cd2e1c1876a33d8eab6c993993933d23c021c59c6663cb525
5
5
  SHA512:
6
- metadata.gz: 9261164e029e28e9303f30879740f5dbd3ccbb6779b0767cd8488920e56f0037f63159fcdf64bd12dbb7788a22f5e1718fc468f616a02f59553a67713dfa422c
7
- data.tar.gz: 5856204cfe3ff27016d1e0f5a54f3de43cf839911fae1dba656200679c941c837b4f30ded9e9f94153ecea2972c8797a0554f81c58df27123f1dc0d66ed7b30a
6
+ metadata.gz: bcdda91582a8b87d3ba5f890f22a0df72c9aed567860c14e06eb85dca5980c2adc735a29a0fa1bbcd9aea3e8b8bdf79d8d88aa6ee88b7231afc9dbdc11edc6d8
7
+ data.tar.gz: 289e1667b14dd0b578c46d7623fc751489ddb4c8d9dfd09f6024d4355fe69a4b5e375275c3395c541c6f97c26e7ce953b378c1e44d2fd8eda03e5346fa319b8c
@@ -352,6 +352,68 @@ body.chat-fullscreen {
352
352
  margin: 0;
353
353
  }
354
354
 
355
+ /* Table styling */
356
+ .comment-content table {
357
+ border-collapse: collapse;
358
+ width: 100%;
359
+ font-size: 0.85em;
360
+ margin: 0;
361
+ }
362
+
363
+ .comment-content table th,
364
+ .comment-content table td {
365
+ border: 1px solid var(--color-border);
366
+ padding: 0.3em 0.6em;
367
+ text-align: left;
368
+ white-space: nowrap;
369
+ }
370
+
371
+ .comment-content table th {
372
+ background: var(--color-section-bg);
373
+ font-weight: 600;
374
+ }
375
+
376
+ .comment-content table tr:hover td {
377
+ background: color-mix(in srgb, var(--color-section-bg) 40%, transparent);
378
+ }
379
+
380
+ /* Table download wrapper */
381
+ .table-download-wrapper {
382
+ position: relative;
383
+ overflow-x: auto;
384
+ margin: 0.4em 0;
385
+ }
386
+
387
+ .table-download-toolbar {
388
+ display: flex;
389
+ justify-content: flex-end;
390
+ gap: 0.3em;
391
+ padding: 0.15em 0;
392
+ opacity: 0;
393
+ transition: opacity 0.15s ease;
394
+ }
395
+
396
+ .table-download-wrapper:hover .table-download-toolbar {
397
+ opacity: 1;
398
+ }
399
+
400
+ .table-download-btn {
401
+ background: none;
402
+ border: 1px solid var(--color-border);
403
+ border-radius: var(--radius-2);
404
+ color: var(--color-muted);
405
+ font-size: 0.75em;
406
+ padding: 0.15em 0.5em;
407
+ cursor: pointer;
408
+ line-height: 1.4;
409
+ transition: color 0.15s ease, border-color 0.15s ease;
410
+ }
411
+
412
+ .table-download-btn:hover {
413
+ color: var(--color-active);
414
+ border-color: var(--color-active);
415
+ }
416
+
355
417
  /* Quote indicator in form */
356
418
  .comment-quote-indicator {
357
419
  display: flex;
@@ -1,5 +1,6 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
  import { renderCommentMarkdown } from '../lib/utils/markdown'
3
+ import { addTableDownloadButtons } from '../lib/utils/table_download'
3
4
  import CommonPopup from '../lib/common_popup'
4
5
 
5
6
  // Global tracker: persists streaming state across Turbo replacements
@@ -110,6 +111,7 @@ export default class extends Controller {
110
111
  this._resetStreamingTimeout()
111
112
  } else {
112
113
  contentElement.innerHTML = renderCommentMarkdown(text)
114
+ addTableDownloadButtons(contentElement)
113
115
  contentElement.classList.remove('streaming')
114
116
  if (this._isStreaming) this._cleanupStreaming()
115
117
  }
@@ -1,5 +1,6 @@
1
1
  import { marked } from 'marked'
2
2
  import DOMPurify from 'dompurify'
3
+ import { addTableDownloadButtons } from './table_download'
3
4
  import hljs from 'highlight.js/lib/core'
4
5
 
5
6
  // Register only commonly used languages to keep the bundle small
@@ -129,5 +130,6 @@ export function renderMarkdownInContainer(container) {
129
130
  if (element.dataset.rendered === 'true') return
130
131
  element.innerHTML = renderCommentMarkdown(element.textContent)
131
132
  element.dataset.rendered = 'true'
133
+ addTableDownloadButtons(element)
132
134
  })
133
135
  }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Table Download Utility
3
+ *
4
+ * Detects rendered <table> elements within comment content and adds
5
+ * download buttons (CSV / Excel) above each table.
6
+ */
7
+
8
+ function getI18n(commentEl) {
9
+ const popup = commentEl.closest('#comments-popup')
10
+ return {
11
+ csv: popup?.dataset?.tableDownloadCsvText || 'CSV',
12
+ excel: popup?.dataset?.tableDownloadExcelText || 'Excel',
13
+ }
14
+ }
15
+
16
+ function parseTable(table) {
17
+ const rows = []
18
+ table.querySelectorAll('tr').forEach((tr) => {
19
+ const cells = []
20
+ tr.querySelectorAll('th, td').forEach((cell) => {
21
+ cells.push(cell.textContent.trim())
22
+ })
23
+ rows.push(cells)
24
+ })
25
+ return rows
26
+ }
27
+
28
+ function escapeCsvCell(value) {
29
+ if (/[",\n\r]/.test(value)) {
30
+ return `"${value.replace(/"/g, '""')}"`
31
+ }
32
+ return value
33
+ }
34
+
35
+ function downloadBlob(blob, filename) {
36
+ const url = URL.createObjectURL(blob)
37
+ const a = document.createElement('a')
38
+ a.href = url
39
+ a.download = filename
40
+ document.body.appendChild(a)
41
+ a.click()
42
+ document.body.removeChild(a)
43
+ URL.revokeObjectURL(url)
44
+ }
45
+
46
+ function downloadCsv(rows, filename) {
47
+ const csv = rows.map((row) => row.map(escapeCsvCell).join(',')).join('\r\n')
48
+ // BOM for Excel UTF-8 compatibility
49
+ const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8' })
50
+ downloadBlob(blob, filename)
51
+ }
52
+
53
+ function downloadExcel(rows, filename) {
54
+ const escapeXml = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
55
+
56
+ const headerRow = rows[0] || []
57
+ const dataRows = rows.slice(1)
58
+
59
+ let xml = '<?xml version="1.0" encoding="UTF-8"?>\n'
60
+ xml += '<?mso-application progid="Excel.Sheet"?>\n'
61
+ xml += '<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"\n'
62
+ xml += ' xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet">\n'
63
+ xml += ' <Styles>\n'
64
+ xml += ' <Style ss:ID="header"><Font ss:Bold="1"/></Style>\n'
65
+ xml += ' </Styles>\n'
66
+ xml += ' <Worksheet ss:Name="Sheet1">\n'
67
+ xml += ' <Table>\n'
68
+
69
+ // Header row
70
+ if (headerRow.length) {
71
+ xml += ' <Row>\n'
72
+ headerRow.forEach((cell) => {
73
+ xml += ` <Cell ss:StyleID="header"><Data ss:Type="String">${escapeXml(cell)}</Data></Cell>\n`
74
+ })
75
+ xml += ' </Row>\n'
76
+ }
77
+
78
+ // Data rows
79
+ dataRows.forEach((row) => {
80
+ xml += ' <Row>\n'
81
+ row.forEach((cell) => {
82
+ const type = isNaN(cell) || cell === '' ? 'String' : 'Number'
83
+ xml += ` <Cell><Data ss:Type="${type}">${escapeXml(cell)}</Data></Cell>\n`
84
+ })
85
+ xml += ' </Row>\n'
86
+ })
87
+
88
+ xml += ' </Table>\n'
89
+ xml += ' </Worksheet>\n'
90
+ xml += '</Workbook>'
91
+
92
+ const blob = new Blob([xml], { type: 'application/vnd.ms-excel;charset=utf-8' })
93
+ downloadBlob(blob, filename)
94
+ }
95
+
96
+ function generateFilename(table, index) {
97
+ // Try to use the first header cell as a hint for the filename
98
+ const firstHeader = table.querySelector('th')
99
+ if (firstHeader) {
100
+ const hint = firstHeader.textContent.trim().slice(0, 30).replace(/[^a-zA-Z0-9가-힣_-]/g, '_')
101
+ if (hint) return `table_${hint}`
102
+ }
103
+ return `table_${index + 1}`
104
+ }
105
+
106
+ function createToolbar(table, index, i18n) {
107
+ const toolbar = document.createElement('div')
108
+ toolbar.className = 'table-download-toolbar'
109
+
110
+ const csvBtn = document.createElement('button')
111
+ csvBtn.type = 'button'
112
+ csvBtn.className = 'table-download-btn'
113
+ csvBtn.textContent = `\u2913 ${i18n.csv}`
114
+ csvBtn.addEventListener('click', (e) => {
115
+ e.preventDefault()
116
+ e.stopPropagation()
117
+ const rows = parseTable(table)
118
+ const filename = generateFilename(table, index)
119
+ downloadCsv(rows, `${filename}.csv`)
120
+ })
121
+
122
+ const excelBtn = document.createElement('button')
123
+ excelBtn.type = 'button'
124
+ excelBtn.className = 'table-download-btn'
125
+ excelBtn.textContent = `\u2913 ${i18n.excel}`
126
+ excelBtn.addEventListener('click', (e) => {
127
+ e.preventDefault()
128
+ e.stopPropagation()
129
+ const rows = parseTable(table)
130
+ const filename = generateFilename(table, index)
131
+ downloadExcel(rows, `${filename}.xls`)
132
+ })
133
+
134
+ toolbar.appendChild(csvBtn)
135
+ toolbar.appendChild(excelBtn)
136
+ return toolbar
137
+ }
138
+
139
+ /**
140
+ * Scans the given content element for <table> tags and adds download
141
+ * toolbars. Call this after markdown has been rendered into HTML.
142
+ */
143
+ export function addTableDownloadButtons(contentElement) {
144
+ if (!contentElement) return
145
+
146
+ const tables = contentElement.querySelectorAll('table')
147
+ if (!tables.length) return
148
+
149
+ const i18n = getI18n(contentElement)
150
+
151
+ tables.forEach((table, index) => {
152
+ // Skip if already wrapped
153
+ if (table.parentElement?.classList.contains('table-download-wrapper')) return
154
+
155
+ const wrapper = document.createElement('div')
156
+ wrapper.className = 'table-download-wrapper'
157
+
158
+ const toolbar = createToolbar(table, index, i18n)
159
+ table.parentNode.insertBefore(wrapper, table)
160
+ wrapper.appendChild(toolbar)
161
+ wrapper.appendChild(table)
162
+ })
163
+ }
@@ -59,7 +59,9 @@
59
59
  data-review-type-review-label="<%= t('collavre.comments.review_type_review') %>"
60
60
  data-review-type-question-label="<%= t('collavre.comments.review_type_question') %>"
61
61
  data-remove-from-history-label="<%= t('collavre.comments.remove_from_history') %>"
62
- data-inbox-reply-button="<%= t('collavre.comments.inbox_reply_button') %>">
62
+ data-inbox-reply-button="<%= t('collavre.comments.inbox_reply_button') %>"
63
+ data-table-download-csv-text="<%= t('collavre.comments.table_download.csv') %>"
64
+ data-table-download-excel-text="<%= t('collavre.comments.table_download.excel') %>">
63
65
  <div class="resize-handle resize-handle-left" data-comments--popup-target="leftHandle"></div>
64
66
  <div class="resize-handle resize-handle-right" data-comments--popup-target="rightHandle"></div>
65
67
  <div class="comments-popup-header" data-comments--popup-target="header">
@@ -196,6 +196,9 @@ en:
196
196
  stop_agent: Stop
197
197
  read_by: Read by %{name}
198
198
  activity_logs_summary: Activity Logs
199
+ table_download:
200
+ csv: CSV
201
+ excel: Excel
199
202
  lightbox:
200
203
  close: Close
201
204
  previous: Previous
@@ -193,6 +193,9 @@ ko:
193
193
  stop_agent: 중지
194
194
  read_by: "%{name} 님이 읽음"
195
195
  activity_logs_summary: 활동 기록
196
+ table_download:
197
+ csv: CSV
198
+ excel: Excel
196
199
  lightbox:
197
200
  close: 닫기
198
201
  previous: 이전
@@ -1,3 +1,3 @@
1
1
  module Collavre
2
- VERSION = "0.14.0"
2
+ VERSION = "0.15.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: collavre
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Collavre
@@ -321,6 +321,7 @@ files:
321
321
  - app/javascript/lib/touch_drag.js
322
322
  - app/javascript/lib/turbo_stream_actions.js
323
323
  - app/javascript/lib/utils/markdown.js
324
+ - app/javascript/lib/utils/table_download.js
324
325
  - app/javascript/modules/__tests__/creative_progress.test.js
325
326
  - app/javascript/modules/command_args_form.js
326
327
  - app/javascript/modules/command_menu.js