Compare commits

...

14 commits
0.1.1 ... main

Author SHA1 Message Date
cverorg
abd4110b12 docs: point code contributions to the now-public source at CVERInc/tile
The plugin source is open-sourced at github.com/CVERInc/tile (MIT). This repo stays the
release mirror; code PRs now go to the source repo instead of the old issue-only flow.
2026-07-14 16:16:10 +09:00
CVER Inc.
0c90072c04
docs: make language-nav anchors absolute for the Obsidian listing (#5)
In the Obsidian community plugin browser the README renders standalone, so
in-document #anchors don't scroll. Absolute repo anchors instead open the
GitHub README at that section (a real response beats none); on GitHub they
still behave as same-page jumps. Completes the link fix — no relative links or
bare anchors remain.

Co-authored-by: cverorg <292680828+cverorg@users.noreply.github.com>
2026-07-14 14:20:16 +09:00
CVER Inc.
ade0da6e31
chore: add tracked pre-push hook that runs scripts/test.sh (#4)
Completes the clikae-style local guard: a tracked hooks/pre-push (activate with
`git config core.hooksPath hooks`) runs the same scripts/test.sh that CI runs,
so failures surface locally before a push. Bypassable with --no-verify.

Co-authored-by: cverorg <292680828+cverorg@users.noreply.github.com>
2026-07-14 14:20:09 +09:00
CVER Inc.
e359e34d49
chore: add release-integrity CI for the thin publish repo (#3)
No source lives here (the plugin is built in the private tile monorepo), so
the meaningful check is release integrity, not a build: manifest required
fields, manifest version present in versions.json with a matching
minAppVersion, and that the bundled main.js parses. Adds scripts/test.sh (the
single local entrypoint) and a CI workflow that runs it — matching the CVER
per-repo standard, scoped to what a publish repo can actually verify.

Co-authored-by: cverorg <292680828+cverorg@users.noreply.github.com>
2026-07-14 11:48:29 +09:00
CVER Inc.
f6af0f5aab
docs: absolute README links so they survive the Obsidian listing (#2)
The Obsidian community plugin browser renders README.md standalone with no
base URL, so relative links (./CJK.md, ../../releases/latest) 404 there even
though they work on GitHub. Point them at full github.com URLs so they resolve
in both contexts. Language-nav anchors are left as-is (in-document, not dead
links).

Co-authored-by: cverorg <292680828+cverorg@users.noreply.github.com>
2026-07-14 11:48:25 +09:00
chodaict
46c8c89376 Release 0.1.10 — keyboard-complete board + tested/documented CJK
- The full create → sort → edit → archive loop now works without the mouse:
  Space / A / D check, archive, or delete a focused card; arrow keys tug a
  tile within or across lanes; Tab to a lane header and arrow-reorder the
  whole lane; new / rename lane are rebindable commands.
- IME composition is guarded on every keyboard surface (the editor, card
  submit, and the board's move keys), so a Chinese / Japanese / Korean
  candidate-confirm is never mistaken for a shortcut.
- Long-card collapse is width-aware for CJK (fullwidth chars count as two).
- New CJK.md documents the six CJK guarantees and how each is verified.
- README: keyboard + CJK highlights refreshed across all four languages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:00:46 +09:00
chodaict
5a1483968a docs: drop filler word in README origin line
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 21:55:20 +09:00
mixflavor
510c122424 release 0.1.8 — drop the last !important from the stylesheet
Internal CSS cleanup: the stylesheet now relies entirely on selector specificity instead of !important (the leftacts icon-margin reset and the Plain-mode neutralizer were the last two). No visible change; clears the community-review 'Avoid !important' lint.

Co-Authored-By: chodaict <5516582+chodaict@users.noreply.github.com>
2026-06-22 12:12:24 +09:00
mixflavor
0f6d911987 release 0.1.7 — search/rename/add-lane become in-content toolbar bars
Search, lane rename and add-lane no longer open a modal — each takes over the top toolbar row as an in-content bar, so there's no floating dialog and no virtual-keyboard black gap on iOS/iPadOS (the board height is pinned while a bar is open). Tap anywhere off a bar to dismiss it; only one is open at a time.

Also clears the remaining review-bot !important warnings down to the single legitimate Plain-mode reset, and removes dead code left from the old inline inputs.

Co-Authored-By: chodaict <5516582+chodaict@users.noreply.github.com>
2026-06-22 11:48:46 +09:00
mixflavor
ab38af3aeb release 0.1.6 — Obsidian review hygiene + own-DOM search input
Cut the automated review-bot CSS lint without touching theme/iOS/gesture-
defensive overrides: dedupe height/max-height (100dvh only), replace
.modal-container:has(...) with an onOpen container class, style toolbar
scrollbars via ::-webkit-scrollbar, and rebuild the search input as our own
contenteditable (no native <input> chrome). Adds CONTRIBUTING.

Co-Authored-By: chodaict <5516582+chodaict@users.noreply.github.com>
2026-06-20 17:57:08 +09:00
mixflavor
c3eb2c6d8f release 0.1.5 — allow saving a blank tile
Pressing Save (✓) now always keeps the card, even a freshly added one
left empty — tugtile boards intentionally use blank spacer tiles. The
changed-your-mind path is unchanged: Cancel/Escape on an untouched fresh
card still discards it, so an accidental "add card" leaves no junk.

Co-Authored-By: chodaict <5516582+chodaict@users.noreply.github.com>
2026-06-18 20:02:32 +09:00
mixflavor
a523a5d2ab release 0.1.4 — cap the max archive size (auto-trim oldest archived cards)
Belated public release: this shipped in the dev build but never got cut.
Archived lanes now honour a configurable max archive size and trim the
oldest cards once the cap is exceeded, so an archive can't grow unbounded.

Co-Authored-By: chodaict <5516582+chodaict@users.noreply.github.com>
2026-06-18 20:02:05 +09:00
mixflavor
5cb6d69483 release 0.1.3 — card-table wording + cleaner CJK punctuation; backup history limit replaces max stash size
Co-Authored-By: chodaict <x@cver.net>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:21:44 +09:00
mixflavor
373048d9ee release 0.1.2 — fold-all cycle logic lifted into shared core (no behaviour change)
Co-Authored-By: chodaict <x@cver.net>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:04:51 +09:00
10 changed files with 750 additions and 239 deletions

18
.github/workflows/ci.yml vendored Normal file
View 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
View 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
`- [] …` (fullwidth ) — 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
View 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).

View file

@ -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
- **カードを失わない**:取り消し/やり直しを内蔵、最初の書き込み前にバックアップ、
ファイルが外部で変わったら上書きせず再読み込み。(データ安全テストで保証)
- **手はキーボードから離れない**矢印キーでタイルを同じ列隣の列へ動かし、SpaceAD
で完了・アーカイブ・削除、Tab で列見出しへ移って矢印キーで列ごと並べ替え。列の新規作成
/名前変更は割り当て可能なコマンド。ボード一枚、マウスに触れずに片づきます。
- **スマホで生き残るエディタ**2 段ツールバー、iOSiPad の仮想キーボードに追従する
レイアウト。列もタイルもつまんで並べ替え、日付/時刻、タグ、アーカイブ。
- **データはずっと 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을 위해 쓴 플러그인들(플러그인의
- **카드를 잃지 않습니다**: 실행 취소/다시 실행 내장, 첫 쓰기 전 백업, 파일이 외부에서
바뀌면 덮어쓰지 않고 다시 읽기.(데이터 안전 테스트로 보장)
- **손이 키보드를 떠나지 않습니다**: 화살표 키로 타일을 같은 열이나 옆 열로 옮기고,
SpaceAD로 완료・보관・삭제, 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 寫的一堆 pluginplugin 的 plug
- **絕不掉牌**:內建復原/重做、首次寫入前先備份、外部改檔時自動重載而非覆寫
(由資料安全測試套件守護)。
- **雙手永遠在鍵盤上**:方向鍵把一張牌拖到同列或隔壁列,`Space``A``D` 勾選/封存/
刪牌,`Tab` 到牌列標題再用方向鍵搬整條牌列;新增/改名牌列是可自綁熱鍵的指令。整桌牌
理完,不用碰滑鼠。
- **在手機上活得下來的編輯器**雙排工具列、iOSiPad 虛擬鍵盤自動避讓;牌列與牌都能
拖曳排序,加上日期/時間、標籤、收牌(牌庫)。
- **資料永遠是 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
View 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

File diff suppressed because one or more lines are too long

View file

@ -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
View 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"

View file

@ -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) {

View file

@ -1,3 +1,3 @@
{
"0.1.1": "1.4.0"
"0.1.10": "1.4.0"
}