@lnsy/data-table 0.1.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/LICENSE +26 -0
- package/README.md +229 -0
- package/package.json +77 -0
- package/src/data-table.css +327 -0
- package/src/file-io.js +203 -0
- package/src/formula.js +632 -0
- package/src/index.js +20 -0
- package/src/table-component.js +1031 -0
- package/src/wikilinks.js +71 -0
- package/styles/fonts.css +111 -0
- package/styles/variables.css +151 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
The Unlicense
|
|
2
|
+
|
|
3
|
+
This is free and unencumbered software released into the public domain.
|
|
4
|
+
|
|
5
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
6
|
+
distribute this software, either in source code form or as a compiled
|
|
7
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
8
|
+
means.
|
|
9
|
+
|
|
10
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
11
|
+
of this software dedicate any and all copyright interest in the
|
|
12
|
+
software to the public domain. We make this dedication for the benefit
|
|
13
|
+
of the public at large and to the detriment of our heirs and
|
|
14
|
+
successors. We intend this dedication to be an overt act of
|
|
15
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
16
|
+
software under copyright law.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
19
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
20
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
21
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
22
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
23
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
24
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
25
|
+
|
|
26
|
+
For more information, please refer to <https://unlicense.org>
|
package/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# @lnsy/data-table
|
|
2
|
+
|
|
3
|
+
A dependency-free tabular data component (`<data-table>`) built on vanilla
|
|
4
|
+
JS, CSS and HTML — no jspreadsheet, no grid library.
|
|
5
|
+
|
|
6
|
+
One custom element, three presentation modes, all backed by the same data
|
|
7
|
+
model and formula engine.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
### Three modes, one engine
|
|
12
|
+
|
|
13
|
+
Pick a presentation with the `mode` attribute. The data model, formulas and
|
|
14
|
+
`[[wikilinks]]` behave identically in all three:
|
|
15
|
+
|
|
16
|
+
| Mode | Attribute | What you get |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Spreadsheet | `mode="spreadsheet"` (default) | Full grid with A/B/C column headers and numbered rows, a formula bar, range selection, in-cell editing, and resizable columns and rows. |
|
|
19
|
+
| Table | `mode="table"` | Searchable, sortable record table. The first data row becomes the header; click a header to sort ascending/descending (numeric-aware); a live counter shows filtered vs. total records. |
|
|
20
|
+
| List | `mode="list"` | Searchable single-column list built from the first column; empty cells are skipped. |
|
|
21
|
+
|
|
22
|
+
### Spreadsheet editing
|
|
23
|
+
|
|
24
|
+
- Click to select; drag or Shift+Arrow to extend a range selection; arrow
|
|
25
|
+
keys and Tab/Shift+Tab move the active cell.
|
|
26
|
+
- Double-click, press Enter, or just start typing to edit a cell. Enter
|
|
27
|
+
commits and moves down, Tab/Shift+Tab commits and moves sideways, Escape
|
|
28
|
+
cancels.
|
|
29
|
+
- Delete/Backspace clears the selection.
|
|
30
|
+
- Drag the edge of a column or row header to resize it; double-click the
|
|
31
|
+
edge to snap back to the default size.
|
|
32
|
+
- The formula bar shows the raw contents of the active cell (formulas
|
|
33
|
+
included), and cells that contain formulas are visually flagged.
|
|
34
|
+
- `read-only` disables all editing while keeping selection, navigation and
|
|
35
|
+
copy.
|
|
36
|
+
|
|
37
|
+
### Formulas
|
|
38
|
+
|
|
39
|
+
Cells starting with `=` are evaluated by the built-in, dependency-free
|
|
40
|
+
formula engine (`src/formula.js`):
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
=SUM(A1:A5) =IF(A1>2, "yes", "no")
|
|
44
|
+
=A1&B1 =ROUND(A1*B1, 2)
|
|
45
|
+
=IFERROR(HTTP(…), "n/a") =CONCAT("Q", 1)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **References** — `A1`, absolute `\$B\$2`, and ranges `A1:B10` (ranges are
|
|
49
|
+
valid inside functions).
|
|
50
|
+
- **Operators** — `+ - * / ^ %` (modulo), `&` (string concat), and
|
|
51
|
+
comparisons `= <> < > <= >=`, with parentheses and unary minus.
|
|
52
|
+
- **Literals** — numbers, `"strings"` (escape `"` as `""`), `TRUE`/`FALSE`.
|
|
53
|
+
|
|
54
|
+
| Category | Functions |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| Math | `SUM`, `AVERAGE`/`AVG`, `MIN`, `MAX`, `COUNT`, `COUNTA`, `ABS`, `ROUND`, `FLOOR`, `CEILING`/`CEIL`, `SQRT`, `POWER`/`POW`, `MOD` |
|
|
57
|
+
| Text | `CONCAT`/`CONCATENATE`, `LEN`, `UPPER`, `LOWER`, `TRIM` |
|
|
58
|
+
| Logic | `IF`, `IFERROR`, `AND`, `OR`, `NOT` |
|
|
59
|
+
| Date | `TODAY`, `NOW` |
|
|
60
|
+
|
|
61
|
+
`IF` and `IFERROR` short-circuit: only the taken branch is evaluated, which
|
|
62
|
+
matters when a branch hits the network. Circular references are detected and
|
|
63
|
+
reported as `#CIRCULAR!`; other errors surface as `#SYNTAX!`, `#NAME?`,
|
|
64
|
+
`#HTTP!` or `#NOENDPOINT!`.
|
|
65
|
+
|
|
66
|
+
### Live data from formulas
|
|
67
|
+
|
|
68
|
+
Formulas can fetch over HTTP, with dotted-path extraction into the parsed
|
|
69
|
+
JSON (numeric segments index arrays):
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
=HTTP("https://api.example.com/status")
|
|
73
|
+
=HTTP("https://api.frankfurter.app/latest?from=USD", "rates.EUR")
|
|
74
|
+
=HTTPJSON("https://…", "items.0.name")
|
|
75
|
+
=ENDPOINT("rates", "rates.EUR")
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
- `HTTP(url)` returns the response body (text, or stringified JSON); pass a
|
|
79
|
+
second argument to extract a value from the parsed JSON.
|
|
80
|
+
- `HTTPJSON(url, path)` always parses the body as JSON.
|
|
81
|
+
- `ENDPOINT("name", path?)` reads a URL registered from JavaScript:
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
table.setEndpoints({ rates: 'https://api.frankfurter.app/latest?from=USD' });
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Responses are cached for 30 seconds and concurrent identical requests are
|
|
88
|
+
deduplicated; failed requests are only cached for 3 seconds so retries
|
|
89
|
+
happen quickly.
|
|
90
|
+
|
|
91
|
+
### Wikilinks
|
|
92
|
+
|
|
93
|
+
`[[Page Name]]` and `[[Page Name|label]]` render as links inside any cell:
|
|
94
|
+
|
|
95
|
+
- Set `wikilink-base-url="https://…/"` and links open
|
|
96
|
+
`${base}${encodeURIComponent(target)}` in a new tab.
|
|
97
|
+
- Otherwise, clicking a link dispatches a bubbling `wikilink-navigate` event
|
|
98
|
+
with `{ detail: { target } }`, which the host application can handle
|
|
99
|
+
(routing, filtering, opening a panel…).
|
|
100
|
+
|
|
101
|
+
### Copy & paste
|
|
102
|
+
|
|
103
|
+
Ctrl/Cmd+C/X/V work inside the grid and interoperate with Excel, Numbers
|
|
104
|
+
and Google Sheets via tab-separated values. Pasting a multi-cell block
|
|
105
|
+
expands the grid to fit. Programmatic equivalents: `selectionToTSV()`,
|
|
106
|
+
`copySelection()` and `pasteText(text)`.
|
|
107
|
+
|
|
108
|
+
### File I/O
|
|
109
|
+
|
|
110
|
+
The helpers in `src/file-io.js` load, save, import and export an element's
|
|
111
|
+
data. Wire them to your own buttons or menus; they operate on any
|
|
112
|
+
`<data-table>` instance:
|
|
113
|
+
|
|
114
|
+
| Function | Direction | Format |
|
|
115
|
+
| --- | --- | --- |
|
|
116
|
+
| `saveLSS(table, filename)` / `loadLSS(table, file)` | export / import | Native `.lss` (JSON; keeps formulas as `=…` strings; reads older v1/v2 files) |
|
|
117
|
+
| `exportCSV(table, filename)` / `importCSV(table, file)` | export / import | CSV (export writes computed values, like spreadsheet apps) |
|
|
118
|
+
| `exportJSON(table, filename)` / `importJSON(table, file)` | export / import | JSON two-dimensional array |
|
|
119
|
+
| `importSpreadsheetFile(table, file)` | import | XLSX, XLS and ODS |
|
|
120
|
+
|
|
121
|
+
These helpers are the package's only code that uses an npm dependency
|
|
122
|
+
(`xlsx`, for the spreadsheet import); the component itself has none.
|
|
123
|
+
|
|
124
|
+
### Theming
|
|
125
|
+
|
|
126
|
+
Styles ship as `@lnsy/data-table/data-table.css`, scoped to the component
|
|
127
|
+
(`data-table`, `.dt-*`, `a.wikilink`) so nothing leaks into your page. The
|
|
128
|
+
look is driven by CSS custom properties defined in the package's
|
|
129
|
+
`styles/variables.css` — a "dataroom" theme (warm paper, dark-red accents)
|
|
130
|
+
with an automatic dark scheme via `prefers-color-scheme`. Import that file
|
|
131
|
+
or define your own values for the properties (e.g. `--selection-bg`,
|
|
132
|
+
`--grid-cell-height`) to re-theme the component.
|
|
133
|
+
|
|
134
|
+
Single sheet by design.
|
|
135
|
+
|
|
136
|
+
## Install
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
npm install @lnsy/data-table
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Usage
|
|
143
|
+
|
|
144
|
+
Import the module (this registers the `<data-table>` custom element) and the
|
|
145
|
+
stylesheet:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import '@lnsy/data-table';
|
|
149
|
+
import '@lnsy/data-table/data-table.css';
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Then use the element anywhere in your HTML:
|
|
153
|
+
|
|
154
|
+
```html
|
|
155
|
+
<data-table id="grid" mode="spreadsheet" rows="20" cols="8"></data-table>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
The class is also exported for programmatic use:
|
|
159
|
+
|
|
160
|
+
```js
|
|
161
|
+
import DataTable from '@lnsy/data-table';
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Attributes
|
|
165
|
+
|
|
166
|
+
| Attribute | Values | Default |
|
|
167
|
+
| ------------------- | ---------------------------------- | -------------- |
|
|
168
|
+
| `mode` | `spreadsheet` \| `table` \| `list` | `spreadsheet` |
|
|
169
|
+
| `read-only` | boolean | off |
|
|
170
|
+
| `rows` / `cols` | integers (spreadsheet mode) | 50 / 12 |
|
|
171
|
+
| `wikilink-base-url` | URL prefix for `[[wikilinks]]` | — |
|
|
172
|
+
|
|
173
|
+
### Events
|
|
174
|
+
|
|
175
|
+
| Event | Fired when | `detail` |
|
|
176
|
+
| ------------------- | ----------------------------- | ------------------------ |
|
|
177
|
+
| `change` | any cell is edited | `{ row, col, value }` |
|
|
178
|
+
| `selection-change` | the active cell moves | `{ row, col }` |
|
|
179
|
+
| `wikilink-navigate` | a `[[wikilink]]` is clicked | `{ target }` |
|
|
180
|
+
|
|
181
|
+
### Element API
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
const table = document.querySelector('data-table');
|
|
185
|
+
|
|
186
|
+
table.setData([['name', 'qty'], ['nails', '100']]); // replace the contents
|
|
187
|
+
table.getData(); // deep copy of the raw grid
|
|
188
|
+
table.getComputedValue(1, 1); // formula-evaluated cell value
|
|
189
|
+
table.setCellValue(1, 1, '=A1*2');
|
|
190
|
+
table.setEndpoints({ rates: 'https://api.frankfurter.app/latest?from=USD' });
|
|
191
|
+
await table.recalculate(); // re-run every formula and re-render
|
|
192
|
+
table.insertRow(); // append a row (insertRow(5) appends five)
|
|
193
|
+
table.insertColumn(); // append a column
|
|
194
|
+
table.clearSelection(); // blank every cell in the selection
|
|
195
|
+
table.selectionToTSV(); // TSV of the current selection
|
|
196
|
+
table.pasteText('a\tb\nc\td'); // write a TSV block at the active cell
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
This repository is the package's development home; the npm package itself
|
|
202
|
+
only exports `<data-table>` (see `src/index.js`). The demo pages, command
|
|
203
|
+
panel wiring and file menus live in the root `index.js` and are not part of
|
|
204
|
+
the published package.
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm install # install dependencies
|
|
208
|
+
npm start # dev server on http://localhost:3000
|
|
209
|
+
npm run build # production bundle in dist/
|
|
210
|
+
npm run test:unit # vitest unit tests (tests/unit/)
|
|
211
|
+
npm test # playwright end-to-end tests (tests/e2e/)
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Demo pages: `demo.html` links to one demo per feature (endpoints, wikilinks,
|
|
215
|
+
copy-paste, file I/O, table/list modes, live chart).
|
|
216
|
+
|
|
217
|
+
## Publishing
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
npm version patch # or minor / major
|
|
221
|
+
npm publish # prepublishOnly builds and runs the unit tests
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Verify the package contents with `npm pack --dry-run`: only `src/`,
|
|
225
|
+
`styles/`, `README.md` and `LICENSE` ship.
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
[Unlicense](LICENSE) — public domain.
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lnsy/data-table",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A dependency-free <data-table> web component: spreadsheet, table and list modes with formulas, endpoint lookups, wikilinks and file I/O.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"module": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./data-table.css": "./src/data-table.css"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"author": "Lindsey Mysse <lindsey@example.com>",
|
|
13
|
+
"license": "Unlicense",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"data-table",
|
|
16
|
+
"spreadsheet",
|
|
17
|
+
"custom-elements",
|
|
18
|
+
"web-component",
|
|
19
|
+
"vanilla-js",
|
|
20
|
+
"grid",
|
|
21
|
+
"formulas"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/lnsy-dev/data-table.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/lnsy-dev/data-table/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/lnsy-dev/data-table#readme",
|
|
34
|
+
"files": [
|
|
35
|
+
"src/",
|
|
36
|
+
"styles/",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "playwright test",
|
|
42
|
+
"test:ui": "playwright test --ui",
|
|
43
|
+
"test:prod": "npm run build && playwright test --project=production",
|
|
44
|
+
"test:unit": "vitest run",
|
|
45
|
+
"test:mutation": "stryker run",
|
|
46
|
+
"start": "webpack serve",
|
|
47
|
+
"build": "NODE_ENV=production webpack build",
|
|
48
|
+
"prepublishOnly": "npm run build && npm run test:unit"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"xlsx": "^0.18.5"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@lnsy/charts": "file:../charts",
|
|
55
|
+
"@lnsy/command-panel": "^0.1.0",
|
|
56
|
+
"@playwright/test": "^1.62.1",
|
|
57
|
+
"@stryker-mutator/core": "^9.6.1",
|
|
58
|
+
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
59
|
+
"@swc/core": "^1.9.3",
|
|
60
|
+
"copy-webpack-plugin": "^14.0.0",
|
|
61
|
+
"css-loader": "^7.1.2",
|
|
62
|
+
"cssnano": "^7.1.2",
|
|
63
|
+
"dotenv": "^17.2.2",
|
|
64
|
+
"happy-dom": "^20.14.0",
|
|
65
|
+
"html-webpack-plugin": "^5.6.3",
|
|
66
|
+
"mini-css-extract-plugin": "^2.9.2",
|
|
67
|
+
"postcss": "^8.5.6",
|
|
68
|
+
"postcss-loader": "^8.2.0",
|
|
69
|
+
"serve": "^14.2.6",
|
|
70
|
+
"style-loader": "^4.0.0",
|
|
71
|
+
"swc-loader": "^0.2.6",
|
|
72
|
+
"vitest": "^3.2.7",
|
|
73
|
+
"webpack": "^5.98.0",
|
|
74
|
+
"webpack-cli": "^6.0.1",
|
|
75
|
+
"webpack-dev-server": "^5.2.3"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/*
|
|
2
|
+
|
|
3
|
+
<data-table> component stylesheet.
|
|
4
|
+
|
|
5
|
+
Ships with the @lnsy/data-table npm package. Everything here is scoped to
|
|
6
|
+
the component (data-table, .dt-*, a.wikilink) and the body classes it sets
|
|
7
|
+
during column/row resizing.
|
|
8
|
+
|
|
9
|
+
The rules consume the theme custom properties from the aaron-rss
|
|
10
|
+
dataroom theme (--border, --surface, --surface-alt, --muted, --accent,
|
|
11
|
+
--secondary, --font-ui, --font-mono, …), which this file imports. Light
|
|
12
|
+
and dark schemes come from variables.css via `prefers-color-scheme`.
|
|
13
|
+
Page-level styles (fonts, body, headings, buttons) stay in the host
|
|
14
|
+
application's CSS.
|
|
15
|
+
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
@import '../styles/variables.css';
|
|
19
|
+
|
|
20
|
+
data-table {
|
|
21
|
+
display: block;
|
|
22
|
+
font-family: var(--font-mono);
|
|
23
|
+
font-size: 0.85rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
data-table:focus-visible {
|
|
27
|
+
outline: 2px solid var(--accent);
|
|
28
|
+
outline-offset: 2px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.dt-root {
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 0.5rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ── Formula bar ──────────────────────────────────────────── */
|
|
38
|
+
|
|
39
|
+
.dt-formula-bar {
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: stretch;
|
|
42
|
+
gap: 0;
|
|
43
|
+
border: 1px solid var(--border);
|
|
44
|
+
background: var(--background-light);
|
|
45
|
+
box-shadow: var(--box-shadow-inset);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.dt-cell-ref {
|
|
49
|
+
display: inline-flex;
|
|
50
|
+
align-items: center;
|
|
51
|
+
justify-content: center;
|
|
52
|
+
min-width: 4.5rem;
|
|
53
|
+
padding: 0 0.75rem;
|
|
54
|
+
font-family: var(--font-ui);
|
|
55
|
+
font-weight: var(--font-weight-bold);
|
|
56
|
+
color: var(--accent);
|
|
57
|
+
border-right: 1px solid var(--border);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.dt-formula-input {
|
|
61
|
+
flex: 1;
|
|
62
|
+
border: none;
|
|
63
|
+
background: transparent;
|
|
64
|
+
color: var(--foreground);
|
|
65
|
+
font-family: var(--font-mono);
|
|
66
|
+
font-size: 0.9rem;
|
|
67
|
+
padding: 0.35rem 0.6rem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.dt-formula-input:focus {
|
|
71
|
+
outline: none;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* ── Toolbar (table/list modes) ───────────────────────────── */
|
|
75
|
+
|
|
76
|
+
.dt-toolbar {
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 0.75rem;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dt-toolbar label {
|
|
83
|
+
font-family: var(--font-ui);
|
|
84
|
+
color: var(--muted);
|
|
85
|
+
letter-spacing: 0.05em;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.dt-toolbar input[type='search'] {
|
|
89
|
+
flex: 1;
|
|
90
|
+
max-width: 24rem;
|
|
91
|
+
padding: 0.3rem 0.6rem;
|
|
92
|
+
border: 1px solid var(--border);
|
|
93
|
+
background: var(--background-light);
|
|
94
|
+
box-shadow: var(--box-shadow-inset);
|
|
95
|
+
color: var(--foreground);
|
|
96
|
+
font-family: var(--font-body);
|
|
97
|
+
font-size: 0.85rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.dt-toolbar input[type='search']:focus {
|
|
101
|
+
outline: none;
|
|
102
|
+
border-color: var(--accent);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.dt-count {
|
|
106
|
+
font-family: var(--font-ui);
|
|
107
|
+
font-size: 0.8rem;
|
|
108
|
+
color: var(--muted);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ── Grid ─────────────────────────────────────────────────── */
|
|
112
|
+
|
|
113
|
+
.dt-grid-scroll {
|
|
114
|
+
overflow: auto;
|
|
115
|
+
border: 1px solid var(--border);
|
|
116
|
+
max-height: 80vh;
|
|
117
|
+
background: var(--surface);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
table.dt-grid {
|
|
121
|
+
border-collapse: separate;
|
|
122
|
+
border-spacing: 0;
|
|
123
|
+
table-layout: fixed;
|
|
124
|
+
width: max-content;
|
|
125
|
+
min-width: 100%;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.dt-grid th,
|
|
129
|
+
.dt-grid td {
|
|
130
|
+
border-right: 1px solid var(--border);
|
|
131
|
+
border-bottom: 1px solid var(--border);
|
|
132
|
+
padding: 0 0.5rem;
|
|
133
|
+
height: var(--grid-cell-height);
|
|
134
|
+
overflow: hidden;
|
|
135
|
+
white-space: nowrap;
|
|
136
|
+
text-overflow: ellipsis;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.dt-grid th {
|
|
140
|
+
font-family: var(--font-ui);
|
|
141
|
+
font-weight: var(--font-weight-semibold);
|
|
142
|
+
font-size: 0.78rem;
|
|
143
|
+
letter-spacing: 0.04em;
|
|
144
|
+
text-align: center;
|
|
145
|
+
color: var(--muted);
|
|
146
|
+
background: var(--surface-alt);
|
|
147
|
+
position: sticky;
|
|
148
|
+
user-select: none;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.dt-grid thead th {
|
|
152
|
+
top: 0;
|
|
153
|
+
z-index: 2;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.dt-grid .dt-row-header {
|
|
157
|
+
left: 0;
|
|
158
|
+
z-index: 1;
|
|
159
|
+
min-width: 3rem;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.dt-grid thead th.dt-corner {
|
|
163
|
+
left: 0;
|
|
164
|
+
top: 0;
|
|
165
|
+
z-index: 3;
|
|
166
|
+
min-width: 3rem;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.dt-grid td.dt-cell {
|
|
170
|
+
cursor: cell;
|
|
171
|
+
background: var(--surface);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.dt-grid tbody tr:nth-child(even) td.dt-cell {
|
|
175
|
+
background: var(--interactive-hover);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.dt-grid td.dt-in-range {
|
|
179
|
+
background: var(--selection-bg) !important;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.dt-grid td.dt-active {
|
|
183
|
+
outline: 2px solid var(--neutral-1);
|
|
184
|
+
outline-offset: -2px;
|
|
185
|
+
background: var(--surface) !important;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.dt-has-formula {
|
|
189
|
+
color: var(--secondary);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* Cell editor */
|
|
193
|
+
.dt-editor {
|
|
194
|
+
width: 100%;
|
|
195
|
+
height: 100%;
|
|
196
|
+
border: none;
|
|
197
|
+
background: var(--background-light);
|
|
198
|
+
color: var(--foreground);
|
|
199
|
+
font-family: inherit;
|
|
200
|
+
font-size: inherit;
|
|
201
|
+
padding: 0;
|
|
202
|
+
margin: 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.dt-editor:focus {
|
|
206
|
+
outline: none;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* Sortable headers (table mode) */
|
|
210
|
+
th.dt-sortable {
|
|
211
|
+
cursor: pointer;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
th.dt-sortable:hover {
|
|
215
|
+
color: var(--accent);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
th.dt-sorted {
|
|
219
|
+
color: var(--accent);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* ── Column & row resizing (spreadsheet mode) ─────────────── */
|
|
223
|
+
|
|
224
|
+
/* Hover state on a resizable edge: accent line + resize cursor. */
|
|
225
|
+
th.dt-col-resize {
|
|
226
|
+
cursor: col-resize;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
th.dt-row-resize {
|
|
230
|
+
cursor: row-resize;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
th.dt-col-resize::after,
|
|
234
|
+
th.dt-row-resize::after {
|
|
235
|
+
content: '';
|
|
236
|
+
position: absolute;
|
|
237
|
+
background: var(--accent);
|
|
238
|
+
opacity: 0.55;
|
|
239
|
+
pointer-events: none;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
th.dt-col-resize::after {
|
|
243
|
+
top: 0;
|
|
244
|
+
right: -3px;
|
|
245
|
+
width: 6px;
|
|
246
|
+
height: 100%;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
th.dt-row-resize::after {
|
|
250
|
+
left: 0;
|
|
251
|
+
bottom: -3px;
|
|
252
|
+
height: 6px;
|
|
253
|
+
width: 100%;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* Keep the cursor consistent while dragging past the header. */
|
|
257
|
+
body.dt-resizing {
|
|
258
|
+
user-select: none;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
body.dt-col-resizing,
|
|
262
|
+
body.dt-col-resizing * {
|
|
263
|
+
cursor: col-resize !important;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
body.dt-row-resizing,
|
|
267
|
+
body.dt-row-resizing * {
|
|
268
|
+
cursor: row-resize !important;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ── Footer / status ──────────────────────────────────────── */
|
|
272
|
+
|
|
273
|
+
.dt-footer {
|
|
274
|
+
display: flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
gap: 1rem;
|
|
277
|
+
font-family: var(--font-ui);
|
|
278
|
+
font-size: 0.8rem;
|
|
279
|
+
color: var(--muted);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.dt-status {
|
|
283
|
+
color: var(--accent);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/* ── Wikilinks inside cells ───────────────────────────────── */
|
|
287
|
+
|
|
288
|
+
a.wikilink {
|
|
289
|
+
color: var(--color-link);
|
|
290
|
+
font-weight: var(--font-weight-bold);
|
|
291
|
+
text-decoration: underline dotted;
|
|
292
|
+
text-decoration-color: var(--neutral-2);
|
|
293
|
+
text-underline-offset: 0.2em;
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
a.wikilink:hover {
|
|
298
|
+
color: var(--highlight-bright);
|
|
299
|
+
text-decoration-style: solid;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* ── List mode ────────────────────────────────────────────── */
|
|
303
|
+
|
|
304
|
+
ul.dt-list {
|
|
305
|
+
list-style: none;
|
|
306
|
+
padding: 0;
|
|
307
|
+
margin: 0;
|
|
308
|
+
border: 1px solid var(--border);
|
|
309
|
+
background: var(--surface);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
ul.dt-list li {
|
|
313
|
+
position: relative;
|
|
314
|
+
padding: 0.45rem 1rem 0.45rem 2.2rem;
|
|
315
|
+
border-bottom: 1px solid var(--border);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
ul.dt-list li:last-child {
|
|
319
|
+
border-bottom: none;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
ul.dt-list li::before {
|
|
323
|
+
content: '☞';
|
|
324
|
+
position: absolute;
|
|
325
|
+
left: 0.8rem;
|
|
326
|
+
color: var(--color-link);
|
|
327
|
+
}
|