mirror of
https://github.com/cverinc/tugtile.git
synced 2026-07-22 07:42:19 +00:00
Compare commits
14 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abd4110b12 | ||
|
|
0c90072c04 | ||
|
|
ade0da6e31 | ||
|
|
e359e34d49 | ||
|
|
f6af0f5aab | ||
|
|
46c8c89376 | ||
|
|
5a1483968a | ||
|
|
510c122424 | ||
|
|
0f6d911987 | ||
|
|
ab38af3aeb | ||
|
|
c3eb2c6d8f | ||
|
|
a523a5d2ab | ||
|
|
5cb6d69483 | ||
|
|
373048d9ee |
10 changed files with 750 additions and 239 deletions
18
.github/workflows/ci.yml
vendored
Normal file
18
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: release integrity
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: bash scripts/test.sh
|
||||
111
CJK.md
Normal file
111
CJK.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# CJK support in tugtile
|
||||
|
||||
tugtile is built to be a first-class kanban for Chinese, Japanese, and Korean boards — not
|
||||
latin-first with CJK bolted on. This document turns that claim into something you can check:
|
||||
every guarantee below states **what it promises** and **how it is verified** — an automated test
|
||||
where the logic is pure, a short manual repro where a live browser / IME is required.
|
||||
|
||||
The pure logic lives in the engine block (`plugin.src.js`, the `CORE` region: `homeKey`,
|
||||
`displayWidth`, `parseFile`, `serializeFile`, …). It has no Obsidian dependency, so it runs in
|
||||
plain Node and is unit-tested in `test/cjk.test.cjs`.
|
||||
|
||||
## The 6 guarantees
|
||||
|
||||
### 1. Round-trip fidelity for CJK content
|
||||
**Promise:** A card whose body is Chinese / Japanese / Korean text survives `parse → serialize`
|
||||
byte-for-byte. We never re-encode, normalise, or "clean up" your characters — `tile.raw` is the
|
||||
exact markdown from your file and is what we write back.
|
||||
|
||||
**Verified by:** automated — `test/cjk.test.cjs` (`raw line preserved verbatim`,
|
||||
`parse→serialize keeps the fullwidth checkbox line`, `round-trip idempotent`) and the broader
|
||||
round-trip suites in `test/core.test.cjs` / `test/archive-home.test.cjs`, which exercise CJK
|
||||
boards (`## 進行中`, `卡片內容`, `%%tg-home:進行中%%`).
|
||||
|
||||
### 2. Fullwidth / non-ASCII checkbox tolerance
|
||||
**Promise:** The board parser does not assume the checkbox mark is ASCII. A card written
|
||||
`- [X] …` (fullwidth X) — or any single non-ASCII mark inside the `- [ ]` brackets — parses
|
||||
without crashing, and the mark is captured and preserved verbatim.
|
||||
|
||||
**Verified by:** automated — `test/cjk.test.cjs`
|
||||
(`parseFile tolerates fullwidth checkbox mark (no crash)`,
|
||||
`fullwidth checkbox mark captured verbatim`). The parser's matcher is `/^- \[(.)\]/`, capturing
|
||||
any single code point rather than a fixed `[ xX]` set.
|
||||
|
||||
### 3. Display-width awareness (long-card collapse)
|
||||
**Promise:** The "this card is long, collapse it to its title" decision reasons in **visual
|
||||
columns**, not UTF-16 code units. A CJK character occupies ~2 columns, so a 30-character Chinese
|
||||
title (~60 columns wide) collapses at the same *visual* length as a 60-character latin title —
|
||||
neither language is treated as "shorter" than it looks.
|
||||
|
||||
**How it works:** `displayWidth(str)` counts East-Asian wide / fullwidth code points as 2 and
|
||||
everything else as 1, iterating by Unicode code point (so surrogate-pair / astral CJK characters
|
||||
are measured once). The host collapse check is `displayWidth(meta.clean) > 60` — the threshold is
|
||||
**60 columns**, deliberately *not* doubled: 60 latin chars = width 60 (identical to the previous
|
||||
`.length > 60` behaviour), while ~30 CJK chars = width 60.
|
||||
|
||||
**Verified by:** automated — `test/cjk.test.cjs`. `displayWidth` cases: latin-only (`"hello"→5`),
|
||||
CJK (`"理牌"→4`), mixed (`"a理b"→4`), fullwidth punctuation (`":,"→4`), empty (`→0`), kana /
|
||||
hangul / ideographic-space, and a surrogate-pair char (`"𠀀"→2`, not 4). Boundary cases assert
|
||||
latin behaviour is unchanged (60→not long, 61→long) and the CJK collapse point
|
||||
(29 CJK = width 58 → not long; 31 CJK = width 62 → collapses).
|
||||
|
||||
### 4. Fullwidth-space tolerant lane matching
|
||||
**Promise:** A lane named with a fullwidth ideographic space (U+3000, common when typing with an
|
||||
IME) still matches its own archive-restore token. `homeKey()` collapses all whitespace — including
|
||||
U+3000 — to a single ASCII space, on both the writing and the matching side, so an archived card
|
||||
restores to the right lane.
|
||||
|
||||
**Verified by:** automated — `test/cjk.test.cjs` (`homeKey collapses fullwidth space U+3000`,
|
||||
`homeKey fullwidth-space == ascii-space twin`) and the end-to-end archive restore in
|
||||
`test/archive-restore-e2e.test.cjs`.
|
||||
|
||||
### 5. IME composition guard (keyboard surfaces)
|
||||
**Promise:** While you are mid-composition with an IME — picking among Chinese/Japanese candidate
|
||||
characters — pressing Enter or an arrow key confirms / navigates candidates and **never** fires a
|
||||
tugtile shortcut. This is guarded consistently in **all three** keyboard surfaces:
|
||||
- the full-screen **editor** (`editor-core.js` — `compositionstart`/`compositionend` gate the
|
||||
re-highlight, and `Enter`/`Tab` check `!e.isComposing` / `keyCode !== 229`),
|
||||
- **card submit** (`plugin.src.js` — the inline-editor Enter handler checks
|
||||
`!(e.isComposing || e.keyCode === 229)`),
|
||||
- the **board card keydown / arrow handler** (`plugin.src.js` — bails on
|
||||
`e.isComposing || e.keyCode === 229` before any bare-key card action).
|
||||
|
||||
Because composition state is browser/IME-driven, this guarantee is verified by hand, not in Node.
|
||||
|
||||
**Manual repro:**
|
||||
1. Open a board, focus a card's inline editor (or the full-screen editor), and switch your OS to a
|
||||
Chinese or Japanese IME.
|
||||
2. Type a syllable so candidate characters appear (composition is active), then press **Enter** to
|
||||
confirm a candidate — the candidate is inserted; the card must **not** submit/close.
|
||||
3. With a card focused on the board (not its input), start composing and press the **arrow keys** —
|
||||
candidate navigation should work and the card must **not** move between lanes. Releasing the IME
|
||||
and pressing the same keys then behaves normally.
|
||||
|
||||
### 6. CJK line-wrap inside cards
|
||||
**Promise:** A long Chinese / Japanese / Korean title wraps onto multiple lines *inside* the card
|
||||
rather than overflowing or being clipped, so the full title stays readable on the board.
|
||||
|
||||
Wrapping is layout behaviour in a live browser, so it is verified by hand.
|
||||
|
||||
**Manual repro:**
|
||||
1. Create a card with a long single-line CJK title (e.g. 40+ Han characters, no spaces).
|
||||
2. On the board, confirm the title wraps within the card's width and every character is visible
|
||||
(the card grows in height; text is not cut off horizontally).
|
||||
3. Collapse/expand the card (guarantee 3) and confirm the collapsed title line behaves the same.
|
||||
|
||||
## Known limitation (honest disclosure)
|
||||
|
||||
**Table / row view truncates CJK at ~30 visible characters.** The compact row view caps each row's
|
||||
text at `max-width: 60ch` (`styles.css`, the `.tugtile__row-text` rule) with an ellipsis. Because
|
||||
`ch` is the width of the `0` glyph, a CJK character (~2× that width) consumes roughly two `ch`, so
|
||||
CJK row text is visually truncated at about **30 characters** where latin truncates at ~60. This is
|
||||
a **cosmetic** limit of the dense table view only — the full title is always intact in the card
|
||||
view and on disk (round-trip fidelity, guarantee 1, is unaffected). It is documented here rather
|
||||
than hidden; a future fix would switch that cap to a display-width-aware measure.
|
||||
|
||||
## Running the tests
|
||||
|
||||
```
|
||||
node test/cjk.test.cjs # the CJK-specific engine tests
|
||||
bash scripts/test.sh # full suite: syntax + i18n + builds + every test/*.cjs
|
||||
```
|
||||
31
CONTRIBUTING.md
Normal file
31
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Contributing to tugtile
|
||||
|
||||
Thanks for your interest! tugtile is an open-source Obsidian plugin by [CVER Inc.](https://cver.net)
|
||||
|
||||
## Reporting bugs & requesting features
|
||||
|
||||
Please open a [GitHub issue](https://github.com/CVERInc/tugtile/issues). Helpful details:
|
||||
|
||||
- Obsidian version, and your OS / device (desktop, iOS, Android)
|
||||
- Steps to reproduce, and what you expected instead
|
||||
- A small sample of the board Markdown when it's relevant
|
||||
|
||||
## Pull requests
|
||||
|
||||
The built files here (`main.js`, `styles.css`, `manifest.json`) are generated from
|
||||
source, so this repo is the release home — not where code changes are made. **The source
|
||||
is open at [CVERInc/tile](https://github.com/CVERInc/tile)** (MIT): the shared editor
|
||||
engine and both Obsidian plugins live there, and that is where code pull requests go.
|
||||
Clone it, run `bash scripts/test.sh`, then PR against it.
|
||||
|
||||
Documentation wording and translation suggestions are very welcome.
|
||||
|
||||
## Translations
|
||||
|
||||
tugtile ships English, 日本語, 한국어, and 繁體中文. To add or correct a locale, open an
|
||||
issue with the strings and we'll wire it in.
|
||||
|
||||
## License
|
||||
|
||||
By contributing you agree your contributions are licensed under the repository's
|
||||
[MIT License](LICENSE).
|
||||
49
README.md
49
README.md
|
|
@ -1,6 +1,6 @@
|
|||
# tugtile
|
||||
|
||||
[English](#english) · [日本語](#日本語) · [한국어](#한국어) · [繁體中文](#繁體中文)
|
||||
[English](https://github.com/CVERInc/tugtile#english) · [日本語](https://github.com/CVERInc/tugtile#日本語) · [한국어](https://github.com/CVERInc/tugtile#한국어) · [繁體中文](https://github.com/CVERInc/tugtile#繁體中文)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ and tugtile keeps the original markers in place until you choose to upgrade a bo
|
|||
going back stays possible. What you gain on top: undo / redo, an editor built for phones
|
||||
and iPads, and active maintenance.
|
||||
|
||||
tugtile actually started life as plugins *for* obsidian-kanban — until rebuilding the
|
||||
tugtile started life as plugins *for* obsidian-kanban — until rebuilding the
|
||||
whole table was easier than patching someone else's. Written from scratch (clean-room),
|
||||
**MIT**.
|
||||
|
||||
|
|
@ -32,19 +32,26 @@ whole table was easier than patching someone else's. Written from scratch (clean
|
|||
- **Never lose a card** — built-in undo / redo, a backup before the first write, and
|
||||
overwrite protection: if the file changes elsewhere, tugtile reloads instead of
|
||||
clobbering it. (Backed by a data-safety test suite.)
|
||||
- **Hands never leave the keyboard** — arrow keys tug a tile within or across lanes;
|
||||
Space / A / D check, archive, or delete it; Tab to a lane header and arrow-reorder the
|
||||
whole lane; new / rename lane are rebindable commands. Sort a whole board without
|
||||
reaching for the mouse.
|
||||
- **An editor that survives phones** — two-row toolbar, virtual-keyboard-aware layout on
|
||||
iOS/iPad; tug lanes & tiles to reorder, dates/times, tags, stash (archive).
|
||||
- **Your data stays Markdown** — human-readable, diff-friendly, portable. Board and
|
||||
Table views over the same note, plus a raw-editor button when you want the text itself.
|
||||
- **CJK-first, concretely** — re-highlighting waits while your IME is composing, so CJK
|
||||
input is never interrupted; `#tags` accept CJK; sorting is locale-aware.
|
||||
- **Your language isn't an edge case** — every keyboard surface (the editor, card submit,
|
||||
*and* the board's move keys) pauses while your IME is composing, so a Chinese / Japanese
|
||||
/ Korean candidate-confirm is never mistaken for a shortcut; card collapse is
|
||||
width-aware; `#tags` accept CJK. The six CJK guarantees — and how each is verified —
|
||||
live in [CJK.md](https://github.com/CVERInc/tugtile/blob/main/CJK.md).
|
||||
- **No network, no telemetry** — your boards never leave your vault.
|
||||
|
||||
### Install
|
||||
|
||||
- **Community plugins (recommended):** in Obsidian, open Settings → Community plugins →
|
||||
Browse, search for **tugtile**, then Install and Enable.
|
||||
- **Manual:** from the [latest release](../../releases/latest), download `main.js`,
|
||||
- **Manual:** from the [latest release](https://github.com/CVERInc/tugtile/releases/latest), download `main.js`,
|
||||
`manifest.json`, and `styles.css` into your vault's `.obsidian/plugins/tugtile/` folder,
|
||||
then enable **tugtile** in Settings → Community plugins.
|
||||
- **[BRAT](https://github.com/TfTHacker/obsidian42-brat) (beta builds):** add `CVERInc/tugtile`.
|
||||
|
|
@ -83,19 +90,24 @@ tugtile の前身は、obsidian-kanban のために書いた plugin の山(plu
|
|||
|
||||
- **カードを失わない**:取り消し/やり直しを内蔵、最初の書き込み前にバックアップ、
|
||||
ファイルが外部で変わったら上書きせず再読み込み。(データ安全テストで保証)
|
||||
- **手はキーボードから離れない**:矢印キーでタイルを同じ列/隣の列へ動かし、Space/A/D
|
||||
で完了・アーカイブ・削除、Tab で列見出しへ移って矢印キーで列ごと並べ替え。列の新規作成
|
||||
/名前変更は割り当て可能なコマンド。ボード一枚、マウスに触れずに片づきます。
|
||||
- **スマホで生き残るエディタ**:2 段ツールバー、iOS/iPad の仮想キーボードに追従する
|
||||
レイアウト。列もタイルもつまんで並べ替え、日付/時刻、タグ、アーカイブ。
|
||||
- **データはずっと Markdown**:人間が読めて、diff しやすく、持ち運べます。同じノートを
|
||||
ボード/表の 2 ビューで。テキストそのものを触りたいときは raw エディタボタンも。
|
||||
- **CJK ファースト、具体的に**:IME の変換中は再ハイライトを待機し、日本語入力を決して
|
||||
中断しません。`#タグ` も CJK 対応、並べ替えはロケールを認識します。
|
||||
- **あなたの言語は、例外じゃない**:すべてのキーボード操作(エディタ、カード送信、そして
|
||||
ボードの移動キー)は IME の変換中は待機するので、日本語・中国語・韓国語の変換確定が
|
||||
ショートカットと取り違えられることはありません。長いカードの折りたたみは文字幅を考慮し、
|
||||
`#タグ` も CJK 対応。6 つの CJK 保証と、その検証方法は [CJK.md](https://github.com/CVERInc/tugtile/blob/main/CJK.md) に。
|
||||
- **ネットワークなし、テレメトリなし**:ボードが vault の外に出ることはありません。
|
||||
|
||||
### インストール
|
||||
|
||||
- **コミュニティプラグイン(推奨):** 設定 → コミュニティプラグイン → 「閲覧」で
|
||||
**tugtile** を検索し、インストールして有効化。
|
||||
- **手動:** [最新リリース](../../releases/latest) から `main.js`・`manifest.json`・
|
||||
- **手動:** [最新リリース](https://github.com/CVERInc/tugtile/releases/latest) から `main.js`・`manifest.json`・
|
||||
`styles.css` を vault の `.obsidian/plugins/tugtile/` に置き、設定 → コミュニティプラグイン
|
||||
で **tugtile** を有効化。
|
||||
- **[BRAT](https://github.com/TfTHacker/obsidian42-brat)(ベータ版):** `CVERInc/tugtile` を追加。
|
||||
|
|
@ -134,20 +146,25 @@ tugtile은 원래 obsidian-kanban을 위해 쓴 플러그인들(플러그인의
|
|||
|
||||
- **카드를 잃지 않습니다**: 실행 취소/다시 실행 내장, 첫 쓰기 전 백업, 파일이 외부에서
|
||||
바뀌면 덮어쓰지 않고 다시 읽기.(데이터 안전 테스트로 보장)
|
||||
- **손이 키보드를 떠나지 않습니다**: 화살표 키로 타일을 같은 열이나 옆 열로 옮기고,
|
||||
Space/A/D로 완료・보관・삭제, Tab으로 열 머리글로 이동해 화살표 키로 열 전체를 재정렬.
|
||||
열 추가/이름 변경은 단축키를 지정할 수 있는 명령입니다. 보드 하나를 마우스 없이 정리합니다.
|
||||
- **휴대폰에서 살아남는 에디터**: 2단 툴바, iOS/iPad 가상 키보드를 따라가는 레이아웃,
|
||||
열과 타일을 집어서 정렬, 날짜/시간, 태그, 보관함.
|
||||
- **데이터는 언제나 Markdown**: 사람이 읽을 수 있고, diff 친화적이며, 어디로든 가져갈 수
|
||||
있습니다. 같은 노트를 보드/표 두 가지 뷰로, 텍스트를 직접 만지고 싶을 때는 raw 에디터
|
||||
버튼도 있습니다.
|
||||
- **CJK 우선, 구체적으로**: IME 조합 중에는 재하이라이트를 멈춰 한국어 입력이 끊기지
|
||||
않습니다. `#태그`도 CJK 지원, 정렬은 로케일 인식 비교를 사용합니다.
|
||||
- **여러분의 언어는 예외가 아닙니다**: 모든 키보드 입력(에디터, 카드 전송, 그리고 보드의
|
||||
이동 키)이 IME 조합 중에는 대기하므로, 한국어・중국어・일본어의 변환 확정이 단축키로
|
||||
잘못 인식되지 않습니다. 긴 카드 접기는 글자 너비를 고려하고, `#태그`도 CJK 지원. 여섯
|
||||
가지 CJK 보증과 각각의 검증 방법은 [CJK.md](https://github.com/CVERInc/tugtile/blob/main/CJK.md)에 있습니다.
|
||||
- **네트워크 없음, 텔레메트리 없음**: 보드는 vault 밖으로 나가지 않습니다.
|
||||
|
||||
### 설치
|
||||
|
||||
- **커뮤니티 플러그인(권장):** 설정 → 커뮤니티 플러그인 → 「탐색」에서 **tugtile**을
|
||||
검색해 설치하고 활성화하세요.
|
||||
- **수동:** [최신 릴리스](../../releases/latest)에서 `main.js`, `manifest.json`,
|
||||
- **수동:** [최신 릴리스](https://github.com/CVERInc/tugtile/releases/latest)에서 `main.js`, `manifest.json`,
|
||||
`styles.css`를 vault의 `.obsidian/plugins/tugtile/`에 넣고, 설정 → 커뮤니티 플러그인에서
|
||||
**tugtile**을 활성화하세요.
|
||||
- **[BRAT](https://github.com/TfTHacker/obsidian42-brat)(베타 빌드):** `CVERInc/tugtile` 추가.
|
||||
|
|
@ -182,19 +199,23 @@ tugtile 的前身正是幫 obsidian-kanban 寫的一堆 plugin(plugin 的 plug
|
|||
|
||||
- **絕不掉牌**:內建復原/重做、首次寫入前先備份、外部改檔時自動重載而非覆寫
|
||||
(由資料安全測試套件守護)。
|
||||
- **雙手永遠在鍵盤上**:方向鍵把一張牌拖到同列或隔壁列,`Space`/`A`/`D` 勾選/封存/
|
||||
刪牌,`Tab` 到牌列標題再用方向鍵搬整條牌列;新增/改名牌列是可自綁熱鍵的指令。整桌牌
|
||||
理完,不用碰滑鼠。
|
||||
- **在手機上活得下來的編輯器**:雙排工具列、iOS/iPad 虛擬鍵盤自動避讓;牌列與牌都能
|
||||
拖曳排序,加上日期/時間、標籤、收牌(牌庫)。
|
||||
- **資料永遠是 Markdown**:人類可讀、好 diff、帶得走。同一份筆記有牌桌/牌表兩種檢視,
|
||||
想直接碰文字時還有 raw 編輯器按鈕。
|
||||
- **CJK 優先,而且講得出細節**:IME 組字期間暫停重新上色,中文輸入絕不被打斷;`#標籤`
|
||||
支援 CJK;排序使用 locale 感知比較。
|
||||
- **你的語言,不是 edge case**:每一個鍵盤入口(編輯器、送出、連同牌桌的搬牌鍵)都在
|
||||
IME 組字期間讓位,中日韓的選字確認絕不被誤判成快捷鍵;長牌收合是寬度感知的;`#標籤`
|
||||
支援 CJK。六項 CJK 保證與各自的驗證方式都寫在 [CJK.md](https://github.com/CVERInc/tugtile/blob/main/CJK.md)。
|
||||
- **不連網、零遙測**:你的牌桌永遠不會離開 vault。
|
||||
|
||||
### 安裝
|
||||
|
||||
- **社群外掛(推薦):** 在 Obsidian 開 設定 → 社群外掛 → 瀏覽,搜尋 **tugtile**,
|
||||
安裝後啟用。
|
||||
- **手動:** 從[最新 release](../../releases/latest) 下載 `main.js`、`manifest.json`、
|
||||
- **手動:** 從[最新 release](https://github.com/CVERInc/tugtile/releases/latest) 下載 `main.js`、`manifest.json`、
|
||||
`styles.css` 放進 vault 的 `.obsidian/plugins/tugtile/`,然後在 設定 → 社群外掛 啟用
|
||||
**tugtile**。
|
||||
- **[BRAT](https://github.com/TfTHacker/obsidian42-brat)(beta 版):** 加入 `CVERInc/tugtile`。
|
||||
|
|
|
|||
5
hooks/pre-push
Executable file
5
hooks/pre-push
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
# Pre-push guard: run the repo's full check before any push; a failure blocks the push.
|
||||
# Tracked in-repo (not .git/hooks). Activate once with: git config core.hooksPath hooks
|
||||
# Bypass in an emergency with: git push --no-verify (CI still guards server-side).
|
||||
exec bash "$(dirname "$0")/../scripts/test.sh"
|
||||
576
main.js
576
main.js
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "tugtile",
|
||||
"name": "tugtile",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.10",
|
||||
"minAppVersion": "1.4.0",
|
||||
"description": "A card table for your Markdown notes — tug tiles to reorder. Reads your existing kanban boards. CJK-friendly.",
|
||||
"author": "CVER Inc.",
|
||||
|
|
|
|||
20
scripts/test.sh
Executable file
20
scripts/test.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env bash
|
||||
# Release-integrity check for this thin publish repo. The plugin is built in the
|
||||
# private tile monorepo; this repo only ships the artifacts (main.js, manifest,
|
||||
# styles, versions), so there is nothing to compile here. This mirrors CI:
|
||||
# validate manifest/versions consistency and that the bundled main.js parses.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const req = ["id","name","version","minAppVersion","description","author","isDesktopOnly"];
|
||||
const m = JSON.parse(fs.readFileSync("manifest.json","utf8"));
|
||||
for (const k of req) if (!(k in m)) throw new Error(`manifest.json missing "${k}"`);
|
||||
const v = JSON.parse(fs.readFileSync("versions.json","utf8"));
|
||||
if (!(m.version in v)) throw new Error(`manifest version ${m.version} is not listed in versions.json`);
|
||||
if (v[m.version] !== m.minAppVersion) throw new Error(`versions.json[${m.version}]=${v[m.version]} != manifest.minAppVersion=${m.minAppVersion}`);
|
||||
console.log(`manifest/versions OK — ${m.id} ${m.version} (minAppVersion ${m.minAppVersion})`);
|
||||
'
|
||||
node --check main.js
|
||||
echo "main.js parses OK"
|
||||
175
styles.css
175
styles.css
|
|
@ -8,9 +8,9 @@
|
|||
:root { --corner: squircle; }
|
||||
:where(
|
||||
.tugtile-brand, .tugtile__lane, .tugtile__tile, .tugtile__num, .tugtile__popup,
|
||||
.tugtile__popup-d, .tugtile__popup-t, .tugtile__add-btn, .tugtile__add-input, .tugtile__add-ok,
|
||||
.tugtile-prompt-field, .tugtile-iconbtn, .tugtile-ed-find-i, .tugtile__tile-edit,
|
||||
.tugtile__lane-rename, .tugtile-viewcycle, .tugtile__archivebar, .tugtile__lane-chevron,
|
||||
.tugtile__popup-d, .tugtile__popup-t, .tugtile__add-btn,
|
||||
.tugtile-prompt-field, .tugtile-iconbtn, .tugtile-ed-find-i,
|
||||
.tugtile-viewcycle, .tugtile__archivebar, .tugtile__lane-chevron,
|
||||
.tugtile-archive-row, .tugtile__addcol-btn
|
||||
) { corner-shape: var(--corner); }
|
||||
|
||||
|
|
@ -21,7 +21,10 @@
|
|||
}
|
||||
/* Search/Undo/Redo live in the native back/forward container (repurposed by setupLeftActions), so they line up with the left-sidebar toggle above. */
|
||||
.tugtile-leftacts { display: flex; align-items: center; gap: 2px; padding-left: 4px; } /* Nudge right to line up the first icon with the left-sidebar toggle above (auto-measure couldn't find the toggle on iPad) */
|
||||
.tugtile-leftacts > * { margin: 0 !important; } /* Reset Obsidian's per-action margins so Search/Undo/Redo are evenly spaced and aligned */
|
||||
/* iPad only, Obsidian sets `.is-mobile .view-action { margin: auto 0 }` (specificity (0,2,0)) on these addAction
|
||||
icons, which misaligns Search/Undo/Redo. This (0,3,0) compound out-specifies it and resets the margin without
|
||||
!important — the children always carry both classes (addAction → .clickable-icon.view-action). */
|
||||
.tugtile-leftacts > .clickable-icon.view-action { margin: 0; }
|
||||
/* Board⇄table swap: the outgoing snapshot slides up & out while the new view rises from below (two panels shuffling). */
|
||||
.tugtile-viewsnap {
|
||||
position: absolute; inset: 0; z-index: 3;
|
||||
|
|
@ -32,13 +35,6 @@
|
|||
.tugtile--switching .tugtile__board,
|
||||
.tugtile--switching .tugtile__table-wrap { animation: tugtile-view-in 0.38s cubic-bezier(.7, 0, .3, 1); }
|
||||
@keyframes tugtile-view-in { from { transform: translateY(100%); } to { transform: translateY(0); } }
|
||||
/* iOS26 border radius fix (merged from the old kanban-card-radius snippet): iOS26 stretches the core variable --input-radius
|
||||
into a pill shape, deforming any input box or button using it. We override it locally within tugtile's scope (kanban view
|
||||
and the two modals) instead of applying it globally to the body, keeping this OSS plugin clean. Remove once Obsidian/iOS is fixed. */
|
||||
.tugtile,
|
||||
.tugtile-edit-modal-full,
|
||||
.tugtile-prompt-modal,
|
||||
.tugtile-archive-modal { --input-radius: var(--radius-m) !important; }
|
||||
/* Phone: the centered control (view-cycle · lock) sits in a top-bar in the content, not the cramped header */
|
||||
/* Solid bg + z-index above the view-swap snapshot (z-index 3): the bar stays put on top while board/table
|
||||
slide beneath it, so the up-slide animation never leaks through the control row. */
|
||||
|
|
@ -321,8 +317,9 @@ body.is-phone .workspace-leaf-content[data-type="tugtile-board"] .view-content {
|
|||
/* Freeze container scrolling during dragging (adding .is-dragging in main.js onStart) to prevent native mobile scrolling from fighting the drag gesture */
|
||||
.tugtile.is-dragging .tugtile__board,
|
||||
.tugtile.is-dragging .tugtile__list {
|
||||
overflow: hidden !important;
|
||||
touch-action: none !important;
|
||||
/* (0,2,0) already out-specifies the base .tugtile__board/__list (0,1,0) in this file, so no !important needed */
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
}
|
||||
/* Add card button and input */
|
||||
.tugtile__add {
|
||||
|
|
@ -342,20 +339,6 @@ body.is-phone .workspace-leaf-content[data-type="tugtile-board"] .view-content {
|
|||
background: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.tugtile__add-input {
|
||||
width: 100%;
|
||||
min-height: 4em;
|
||||
resize: vertical;
|
||||
border-radius: var(--radius-m);
|
||||
}
|
||||
.tugtile__add-ok {
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border: none;
|
||||
border-radius: var(--radius-s);
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* ⋯ More actions */
|
||||
.tugtile__tile-more {
|
||||
color: var(--text-faint);
|
||||
|
|
@ -384,19 +367,11 @@ body.is-phone .workspace-leaf-content[data-type="tugtile-board"] .view-content {
|
|||
cursor: pointer;
|
||||
color: var(--text-accent);
|
||||
}
|
||||
/* Submit button for mobile editing */
|
||||
.tugtile__edit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 6px;
|
||||
}
|
||||
/* Focused editing sheet (integrated with iOS 26 rounded sheet design; hides native ✕ to use our own left ❌ and right ✓) */
|
||||
.tugtile-edit-modal-full {
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh; /* Dynamic viewport: shrinks with the on-screen keyboard so the editor stays inside the visible area and the textarea can scroll */
|
||||
max-height: 100vh;
|
||||
height: 100dvh; /* Dynamic viewport: shrinks with the on-screen keyboard so the editor stays inside the visible area and the textarea can scroll. dvh is supported on every Obsidian-supported renderer (Electron + iOS/Android webview), so no 100vh fallback is needed. */
|
||||
max-height: 100dvh;
|
||||
border-radius: 0; /* Full-screen focus editor — nothing of the board shows behind */
|
||||
/* Inset content past the iPad status bar / notch / home indicator; env() = 0 on desktop. The solid background still bleeds into these strips. */
|
||||
|
|
@ -406,11 +381,11 @@ body.is-phone .workspace-leaf-content[data-type="tugtile-board"] .view-content {
|
|||
animation: tugtile-pop-in 0.4s cubic-bezier(.34, 1.56, .64, 1); /* Keep the bounce — full-screen ≠ unrefined */
|
||||
}
|
||||
/* Backdrop darkens to solid FAST (so the board vanishes quickly, not lingering behind the bounce); zero container padding */
|
||||
.modal-container:has(.tugtile-edit-modal-full) { padding: 0; align-items: flex-start; } /* Top-pinned, so shrinking the editor to the visual viewport (keyboard) keeps it above the keyboard */
|
||||
.modal-container.tugtile-edit-host { padding: 0; align-items: flex-start; } /* Top-pinned, so shrinking the editor to the visual viewport (keyboard) keeps it above the keyboard. Class set in TileEditModal.onOpen — a direct container target, no parent-match selector. */
|
||||
/* The status-bar / home-indicator strips show --background-secondary (white in light, grey in dark) and sit
|
||||
outside the modal's reach. So instead of fighting to cover them, dye the WHOLE editor --background-secondary
|
||||
to match — seamless in both modes (the old --background-primary only matched in light mode). */
|
||||
.modal-container:has(.tugtile-edit-modal-full) .modal-bg { background: var(--background-secondary); opacity: 1; animation: tugtile-bg-in 0.12s ease both; }
|
||||
.modal-container.tugtile-edit-host .modal-bg { background: var(--background-secondary); opacity: 1; animation: tugtile-bg-in 0.12s ease both; }
|
||||
.tugtile-edit-modal-full,
|
||||
.tugtile-edit-modal-full .tugtile-edit-modal { background: var(--background-secondary); }
|
||||
/* macOS: push the editor down so its top bar (✕) isn't covered by the window traffic lights (class is .mod-macos) */
|
||||
|
|
@ -429,35 +404,21 @@ body.is-phone .workspace-leaf-content[data-type="tugtile-board"] .view-content {
|
|||
from { opacity: 1; transform: scale(1); }
|
||||
to { opacity: 0; transform: scale(0.92); }
|
||||
}
|
||||
/* Small input modal (search / rename / add lane): sit near the top like Spotlight, so the virtual keyboard doesn't cover it. */
|
||||
.modal-container:has(.tugtile-prompt-modal) { align-items: flex-start; }
|
||||
.tugtile-prompt-modal {
|
||||
margin-top: 10vh;
|
||||
transform-origin: center;
|
||||
animation: tugtile-pop-in 0.4s cubic-bezier(.34, 1.56, .64, 1);
|
||||
/* Make the modal PANEL invisible — we stop fighting its box/padding/alignment and just float the search row
|
||||
(field + buttons, which carry their own backgrounds) over a frosted backdrop. */
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
/* Phone: sit below the header toolbar instead of colliding with it (this one was safe; the box/padding
|
||||
overrides that broke the layout — clobbering Obsidian's mobile safe-area — have been removed). */
|
||||
body.is-phone .tugtile-prompt-modal { margin-top: calc(env(safe-area-inset-top) + 64px); }
|
||||
.tugtile-prompt-modal.tugtile-ed-closing { animation: tugtile-pop-out 0.28s ease-in forwards; }
|
||||
.tugtile-prompt-modal .modal-close-button { display: none; } /* Hides Obsidian's native ✕ button to use our custom ❌ button instead */
|
||||
/* Our own inner spacing — replaces Obsidian's 16px .modal-content padding. NEVER touch .modal's box: that
|
||||
carries the mobile safe-area insets (overriding it clipped the buttons off-screen). justify-content:center
|
||||
vertically centres the one-line row, so any leftover modal height is balanced top/bottom (no top-heavy gap). */
|
||||
.tugtile-prompt-modal .modal-content { padding: 8px; justify-content: center; }
|
||||
/* Dim the board behind the search modal (Obsidian forces the prompt backdrop transparent — opacity:0 !important —
|
||||
so without this the board shows at full brightness and crowds the field on a narrow phone). */
|
||||
.modal-container:has(.tugtile-prompt-modal) .modal-bg {
|
||||
opacity: 1 !important;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
backdrop-filter: blur(16px);
|
||||
/* In-content text bars (search · rename · add-lane) — none of these use an Obsidian Modal any more. The view
|
||||
renders a strip at the top of its content: .tugtile-searchbar is persistent (toggled by .tugtile--searching),
|
||||
.tugtile-promptbar is transient (built on demand for rename / add-lane). Both reuse the .tugtile-prompt-row/
|
||||
-field/-input styles below, and the JS freezes the board height while they're open so the virtual keyboard
|
||||
can't collapse it into a black gap. On phone they take over the .tugtile__ctlbar slot. */
|
||||
.tugtile-searchbar { display: none; }
|
||||
.tugtile--searching .tugtile-searchbar,
|
||||
.tugtile-promptbar {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 8px 8px;
|
||||
background: var(--background-primary);
|
||||
}
|
||||
.tugtile--searching .tugtile-searchbar { display: block; }
|
||||
body.is-phone .tugtile--searching .tugtile__ctlbar,
|
||||
body.is-phone .tugtile--prompting .tugtile__ctlbar { display: none; }
|
||||
.tugtile-prompt-row { display: flex; align-items: center; gap: 8px; }
|
||||
/* The input is wrapped in a rounded "field" so a leading icon (search) can sit inside it */
|
||||
.tugtile-prompt-field {
|
||||
|
|
@ -472,22 +433,21 @@ body.is-phone .tugtile-prompt-modal { margin-top: calc(env(safe-area-inset-top)
|
|||
.tugtile-prompt-field:focus-within { border-color: var(--interactive-accent); }
|
||||
.tugtile-prompt-fieldicon { flex: 0 0 auto; display: inline-flex; align-items: center; color: var(--text-muted); }
|
||||
.tugtile-prompt-fieldicon svg { width: 1.15em; height: 1.15em; }
|
||||
/* The input is bare — the .tugtile-prompt-field IS the visible box. High-specificity selector + appearance:none
|
||||
+ !important on every state to fully strip Obsidian's input chrome (the focus border/box-shadow was bigger
|
||||
than the field and broke the nesting). */
|
||||
input.tugtile-prompt-input,
|
||||
input.tugtile-prompt-input:hover,
|
||||
input.tugtile-prompt-input:focus,
|
||||
input.tugtile-prompt-input:focus-visible,
|
||||
input.tugtile-prompt-input:active {
|
||||
flex: 1; min-width: 0; width: auto;
|
||||
-webkit-appearance: none; appearance: none;
|
||||
background: transparent !important;
|
||||
border: 0 !important; border-radius: 0 !important;
|
||||
outline: 0 !important; box-shadow: none !important;
|
||||
padding: 0 !important; margin: 0 !important;
|
||||
height: 100%; min-height: 0;
|
||||
font-size: 16px; color: var(--text-normal); /* 16px prevents iOS auto-zoom on focus */
|
||||
/* The input is our OWN contenteditable element (not a native <input>), so Obsidian's input chrome never
|
||||
applies — no overrides, no !important. The .tugtile-prompt-field IS the visible box; this is the bare
|
||||
single-line text surface inside it. plaintext-only keeps paste/IME clean (no rich markup, CJK-safe). */
|
||||
.tugtile-prompt-input {
|
||||
flex: 1; min-width: 0;
|
||||
outline: none;
|
||||
white-space: pre; overflow-x: auto; overflow-y: hidden; /* single line: scroll sideways, never wrap */
|
||||
font-size: 16px; color: var(--text-normal); /* 16px prevents iOS auto-zoom on focus */
|
||||
-webkit-user-modify: read-write-plaintext-only;
|
||||
}
|
||||
.tugtile-prompt-input::-webkit-scrollbar { display: none; } /* hide the horizontal scrollbar on the one-line field */
|
||||
.tugtile-prompt-input:empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--text-faint);
|
||||
pointer-events: none; /* placeholder text isn't selectable / doesn't block the caret */
|
||||
}
|
||||
/* .tugtile-prompt-x / -ok inherit from .tugtile-iconbtn (the ✓ colour override lives up there) */
|
||||
.tugtile-edit-modal-full .modal-close-button { display: none; } /* Hides the native top-right ✕ button in favor of our custom ❌ button with safety checks */
|
||||
|
|
@ -543,7 +503,7 @@ input.tugtile-prompt-input:active {
|
|||
flex: 1; min-width: 0;
|
||||
display: flex; flex-wrap: nowrap; gap: 4px; align-items: center;
|
||||
justify-content: safe center; /* Centered in the bar (not crammed next to ✕); falls back to start-aligned + scrollable when it overflows */
|
||||
overflow-x: auto; scrollbar-width: thin;
|
||||
overflow-x: auto;
|
||||
}
|
||||
/* .tugtile-ed-tool, .tugtile-ed-find-b, .tugtile-prompt-x/ok all inherit everything from .tugtile-iconbtn above */
|
||||
/* Group divider in the editor toolbar */
|
||||
|
|
@ -553,10 +513,17 @@ input.tugtile-prompt-input:active {
|
|||
flex: 0 0 auto;
|
||||
display: flex; flex-wrap: nowrap; gap: 2px; align-items: center;
|
||||
justify-content: safe center;
|
||||
overflow-x: auto; scrollbar-width: thin; -webkit-overflow-scrolling: touch;
|
||||
overflow-x: auto; -webkit-overflow-scrolling: touch;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
/* Thin horizontal scrollbar for the overflowing toolbar rows, styled via ::-webkit-scrollbar — Obsidian's
|
||||
renderer is Chromium/WebKit on every platform, so this is the portable way to thin the bar (the standard
|
||||
thin-scrollbar property is only partially supported by Obsidian's older builds). */
|
||||
.tugtile-ed-tools::-webkit-scrollbar,
|
||||
.tugtile-ed-tools2::-webkit-scrollbar { height: 6px; }
|
||||
.tugtile-ed-tools::-webkit-scrollbar-thumb,
|
||||
.tugtile-ed-tools2::-webkit-scrollbar-thumb { background: var(--background-modifier-border); border-radius: 3px; }
|
||||
body.is-phone .tugtile-ed-sep { display: none; } /* no group separators in the compact phone rows */
|
||||
/* Phone: pull the editor near the top so the toolbar hugs the Dynamic Island (4px breathing room below it) */
|
||||
body.is-phone .tugtile-edit-modal-full { padding-top: 4px; }
|
||||
|
|
@ -651,15 +618,8 @@ body.is-phone .tugtile-ed-bar { padding-top: 6px; padding-bottom: 4px; }
|
|||
}
|
||||
.is-phone .marktile-toc { width: 100%; border-right: none; } /* phone: full-width overlay (sidebar too cramped) */
|
||||
.is-phone .marktile-toc-open .tugtile-ed-scroll { padding-left: 0; }
|
||||
/* marktile viewcycle → Plain mode: neutralize ALL editor styling so it's a bare plain-text markdown editor
|
||||
(uniform size/weight, no colours, markers shown as-is). Pure CSS — no rendering, no parser, fully portable. */
|
||||
.tugtile-plain .tg-line,
|
||||
.tugtile-plain .tg-line * {
|
||||
font-size: 1em !important; font-weight: normal !important; font-style: normal !important;
|
||||
text-decoration: none !important; color: var(--text-normal) !important; background: transparent !important;
|
||||
font-family: var(--font-monospace) !important; line-height: inherit !important; /* monospace = raw-source feel */
|
||||
border: 0 !important; padding-left: 0 !important; margin: 0 !important;
|
||||
}
|
||||
/* marktile viewcycle → Plain mode neutralizer lives AFTER the syntax rules below — it must out-specify every one
|
||||
of them WITHOUT !important, so it's placed past the last competitor (see the specificity-contract comment there). */
|
||||
/* marktile lock → read-only. contenteditable=false alone is OVERRIDDEN on WebKit by the editor's
|
||||
-webkit-user-modify: read-write-plaintext-only, so flip that to read-only here (the real switch). Also
|
||||
disable the toolbar so a stray tap can't edit (Obsidian has no save step — whatever changes IS saved). */
|
||||
|
|
@ -698,12 +658,24 @@ body.is-phone .tugtile-ed-rich { padding-bottom: 55vh; }
|
|||
.marktile-palette-color .tugtile-ed-rich .tg-code { color: var(--color-green, #98c379); }
|
||||
.marktile-palette-color .tugtile-ed-rich .tg-tag,
|
||||
.marktile-palette-color .tugtile-ed-rich .tg-link { color: var(--color-cyan, #56b6c2); }
|
||||
/* Textarea for card editing */
|
||||
.tugtile__tile-edit {
|
||||
width: 100%;
|
||||
min-height: 4em;
|
||||
resize: vertical;
|
||||
border-radius: var(--radius-s);
|
||||
/* Plain mode = a hard reset of ALL the syntax styling above to a bare monospace baseline. This used to be one
|
||||
!important block; it's now specificity-only (no !important) so styles.css is 100% !important-free.
|
||||
⚠️ SPECIFICITY CONTRACT — do not weaken, and keep this block AFTER every syntax rule above:
|
||||
• every syntax rule above tops out at (0,3,0) — the .marktile-palette-color and .marktile-ed .tg-hN .tg-tab rules.
|
||||
• `.tugtile-plain .tugtile-ed-rich .tg-line[ *]` = (0,3,0): covers non-marktile hosts (tugtile card
|
||||
editor, web), where NO (0,3,0) competitor exists (palette + tab rules are all .marktile-* gated → only (0,2,0)).
|
||||
• `.marktile-ed.tugtile-plain .tugtile-ed-rich .tg-line[ *]` = (0,4,0): clears marktile's (0,3,0) palette + tab rules.
|
||||
.tugtile-plain sits on the editor mount (an ANCESTOR of .tugtile-ed-rich); .tg-line are direct children of it.
|
||||
If you ever add a syntax rule at (0,4,0)+, or a new editor host that runs Plain WITHOUT .marktile-ed, Plain mode
|
||||
silently stops neutralising (symptom: colours/sizes leak through) — fix it here. There is no test guarding this. */
|
||||
.tugtile-plain .tugtile-ed-rich .tg-line,
|
||||
.tugtile-plain .tugtile-ed-rich .tg-line *,
|
||||
.marktile-ed.tugtile-plain .tugtile-ed-rich .tg-line,
|
||||
.marktile-ed.tugtile-plain .tugtile-ed-rich .tg-line * {
|
||||
font-size: 1em; font-weight: normal; font-style: normal;
|
||||
text-decoration: none; color: var(--text-normal); background: transparent;
|
||||
font-family: var(--font-monospace); line-height: inherit; /* monospace = raw-source feel */
|
||||
border: 0; padding-left: 0; margin: 0;
|
||||
}
|
||||
/* Lane header container */
|
||||
.tugtile__lane-title {
|
||||
|
|
@ -721,11 +693,6 @@ body.is-phone .tugtile-ed-rich { padding-bottom: 55vh; }
|
|||
.tugtile__lane--narrowing .tugtile__lane-title { transform: scaleX(0); opacity: 0; }
|
||||
.tugtile__lane--narrowing .tugtile__lane-count,
|
||||
.tugtile__lane--narrowing .tugtile__lane-more { opacity: 0; }
|
||||
.tugtile__lane-rename {
|
||||
flex: 1;
|
||||
font-weight: 700;
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
/* Responsive board (opt-in, default off): on a narrow pane the horizontal lanes reflow into a single full-width vertical stack — the behaviour of the retired List view, now automatic via a container query. Drag-and-drop/editing logic is shared with the board view. */
|
||||
.tugtile--rwd { container-type: inline-size; container-name: tugtileboard; }
|
||||
@container tugtileboard (max-width: 640px) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"0.1.1": "1.4.0"
|
||||
"0.1.10": "1.4.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue