react-masume-grid 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 t92345era
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ja.md ADDED
@@ -0,0 +1,152 @@
1
+ # MasumeGrid
2
+
3
+ [English README is here](README.md)
4
+
5
+ 軽量・汎用の React スプレッドシートコンポーネント。依存は React のみ(gzip 約 5KB)。
6
+
7
+ - **グリッド表示** — 行番号・ヘッダーの表示/非表示切り替え、列幅指定、ドラッグでの列幅リサイズ、行の仮想化描画(数万行でも軽快)
8
+ - **セル型** — 文字列 / 数値(全角・カンマ正規化)/ 選択肢(マスタデータのプルダウン、コード保存・ラベル表示)/ 日付(カレンダー入力、和式表記の貼り付け正規化)
9
+ - **セル編集** — ダブルクリック / F2 / キー入力で編集開始。**日本語 IME 完全対応**(IMEオンで「A」を打つとセルが編集状態になり「あ」が入力される)
10
+ - **範囲選択** — マウスドラッグ、Shift+クリック/矢印キーで拡張、Ctrl(⌘)+クリックで複数範囲追加。行・列ヘッダークリックで行/列選択、左上コーナーで全選択
11
+ - **コピー&ペースト** — Ctrl(⌘)+C / X / V。Excel・Google スプレッドシートと相互運用できる TSV 形式(改行・タブ・引用符を含むセルにも対応、単一セルのタイル貼り付けも可)
12
+
13
+ ## インストール
14
+
15
+ ```sh
16
+ npm install react-masume-grid
17
+ ```
18
+
19
+ ## 使い方
20
+
21
+ ```tsx
22
+ import { useState } from 'react';
23
+ import { MasumeGrid } from 'react-masume-grid';
24
+ // ライブラリを直接 import した場合、CSS は自動で読み込まれます。
25
+ // バンドラー設定によっては明示的に: import 'react-masume-grid/styles.css';
26
+
27
+ function App() {
28
+ const [data, setData] = useState<string[][]>([
29
+ ['りんご', '100', '果物'],
30
+ ['にんじん', '80', '野菜'],
31
+ ]);
32
+
33
+ return (
34
+ <MasumeGrid
35
+ data={data}
36
+ onChange={setData}
37
+ columns={[
38
+ { title: '品名', width: 160 },
39
+ { title: '単価', width: 80 },
40
+ { title: 'カテゴリ', width: 120, readOnly: true },
41
+ ]}
42
+ showRowNumbers
43
+ style={{ height: 400 }}
44
+ />
45
+ );
46
+ }
47
+ ```
48
+
49
+ `columns` を省略すると列数は `data` から導出され、ヘッダーは A, B, C… 表記になります。
50
+
51
+ ## Props
52
+
53
+ | Prop | 型 | 既定値 | 説明 |
54
+ | --- | --- | --- | --- |
55
+ | `data` | `string[][]` | (必須) | グリッドの内容。行の長さは不揃いでも可 |
56
+ | `columns` | `ColumnDef[]` | — | `{ title?, width?, readOnly?, resizable?, type?, options?, strict? }` の配列。省略時は data から列数を導出 |
57
+ | `onChange` | `(next: string[][]) => void` | — | 編集・貼り付け・削除のたびに新しい 2 次元配列で呼ばれる |
58
+ | `onCellChange` | `(row, col, value) => void` | — | 変更セルごとに呼ばれる。`onChange` の代わり/併用可 |
59
+ | `onSelectionChange` | `(ranges: NormalizedRange[]) => void` | — | 選択変更時(`{top,left,bottom,right}` の配列) |
60
+ | `onColumnResize` | `(col, width) => void` | — | 列幅ドラッグの確定時(最終幅 px) |
61
+ | `showRowNumbers` | `boolean` | `true` | 行番号列の表示 |
62
+ | `showHeader` | `boolean` | `true` | ヘッダー行の表示 |
63
+ | `readOnly` | `boolean` | `false` | 編集禁止(選択・コピーは可能) |
64
+ | `resizableColumns` | `boolean` | `true` | ヘッダー境界のドラッグで列幅を変更可能に。列単位は `ColumnDef.resizable` で上書き(要 `showHeader`) |
65
+ | `rowHeight` | `number` | `28` | 行の高さ(px) |
66
+ | `headerHeight` | `number` | `28` | ヘッダーの高さ(px) |
67
+ | `defaultColumnWidth` | `number` | `120` | 幅未指定の列の幅(px) |
68
+ | `rowNumberWidth` | `number` | `48` | 行番号列の幅(px) |
69
+ | `className` / `style` | — | — | ルート要素に適用。高さは `style` や CSS で指定(既定 420px) |
70
+
71
+ データは**制御コンポーネント**方式です。`onChange` を実装しない限りグリッドは変化しません。
72
+
73
+ 列幅のみ例外的に非制御で、ドラッグした幅はコンポーネント内部に保持されます(`ColumnDef.width` より優先)。幅を永続化したい場合は `onColumnResize` で保存してください。
74
+
75
+ ## セル型
76
+
77
+ `ColumnDef.type` で列ごとのセル型を指定できます。**データはすべて文字列のまま**で、型は編集UI・入力の正規化・表示を制御します(クリップボード互換性のため)。
78
+
79
+ ```tsx
80
+ const columns: ColumnDef[] = [
81
+ { title: '商品名' }, // text(既定)
82
+ { title: '単価', type: 'number' },
83
+ { title: 'カテゴリ', type: 'select', options: [
84
+ { value: 'C01', label: '果物' }, // コードを保存、ラベルを表示
85
+ { value: 'C02', label: '青果' },
86
+ ]},
87
+ { title: '状態', type: 'select', options: ['在庫あり', '取り寄せ'] }, // 文字列だけでも可
88
+ { title: '入荷日', type: 'date' },
89
+ ];
90
+ ```
91
+
92
+ | 型 | 編集UI | 動作 |
93
+ | --- | --- | --- |
94
+ | `text` | テキスト(IME対応) | 既定。自由入力 |
95
+ | `number` | テキスト(IME対応) | 右寄せ表示。確定時に全角数字→半角、カンマ除去を正規化。数値でない入力は**拒否**(元の値を保持) |
96
+ | `select` | 絞り込み付きプルダウン | ↑↓で候補移動、Enter/クリックで確定、文字入力で絞り込み。Alt+↓でも開く。`options` は `string` または `{value, label}`(value を保存し label を表示)。既定では options 外の値を拒否(`strict: false` で自由入力許可) |
97
+ | `date` | ネイティブの日付ピッカー | `YYYY-MM-DD` で保存。貼り付け時は `2026/7/6`・`2026年7月6日`・`20260706`・全角も正規化。無効な日付は拒否。Alt+↓でカレンダーを開く |
98
+
99
+ 正規化・検証は**編集確定と貼り付けの両方**に適用されます。無効な値のセルは変更されずスキップされます。正規化関数は `normalizeNumberInput` / `normalizeDateInput` としてエクスポートしているので、アプリ側のバリデーションにも再利用できます。
100
+
101
+ ## キーボード操作
102
+
103
+ | キー | 動作 |
104
+ | --- | --- |
105
+ | 矢印 / Tab / Enter | セル移動(Shift で逆方向・範囲拡張) |
106
+ | PageUp / PageDown | ページ単位移動 |
107
+ | Home / End | 行頭 / 行末(Ctrl+Home/End で先頭 / 末尾セル) |
108
+ | 任意の文字キー | その文字で編集開始(IME 対応) |
109
+ | F2 / ダブルクリック | 既存値を保持したまま編集開始 |
110
+ | Enter / Tab | 編集確定して移動、Esc で取り消し、Alt+Enter でセル内改行 |
111
+ | Delete / Backspace | 選択セルをクリア |
112
+ | Ctrl(⌘)+A | 全選択 |
113
+ | Ctrl(⌘)+C / X / V | コピー / 切り取り / 貼り付け |
114
+
115
+ ## スタイルのカスタマイズ
116
+
117
+ CSS 変数を上書きするだけでテーマを変更できます。
118
+
119
+ ```css
120
+ .my-grid {
121
+ --masume-grid-accent: #0f9d58;
122
+ --masume-grid-sel-bg: rgba(15, 157, 88, 0.12);
123
+ --masume-grid-header-bg: #f0f4f1;
124
+ }
125
+ ```
126
+
127
+ 利用可能な変数は [src/masume-grid.css](src/masume-grid.css) 冒頭を参照してください。
128
+
129
+ ## IME(日本語入力)対応の仕組み
130
+
131
+ グリッドは常にフォーカスされた不可視の `<textarea>` をアクティブセル上に重ねています(Google スプレッドシート等と同じ方式)。`compositionstart` を検知して編集モードへ移行するため、IME の変換候補ウィンドウはセルの位置に表示され、確定の Enter がセル移動として誤処理されることもありません(Safari のイベント順序の差異にも対応済み)。
132
+
133
+ ## 開発
134
+
135
+ ```sh
136
+ npm install
137
+ npm run dev # デモアプリ (http://localhost:5173)
138
+ npm test # ユニットテスト (vitest)
139
+ npm run typecheck # 型チェック
140
+ npm run build # dist/ へライブラリビルド (ESM + CJS + d.ts + CSS)
141
+ ```
142
+
143
+ ## 制限事項(現バージョン)
144
+
145
+ - 内部データは常に文字列(数値・日付も文字列で保持。表示用の書式付け(桁区切り等)は今後の課題)
146
+ - 列は仮想化していないため、数百列を超える場合は性能に注意
147
+ - アンドゥ / リドゥは未実装(`onChange` ベースなので利用側で履歴管理が可能)
148
+ - セル結合、数式、列幅ダブルクリックでの自動フィットは未対応
149
+
150
+ ## License
151
+
152
+ MIT
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # MasumeGrid
2
+
3
+ [日本語版 README はこちら](README.ja.md)
4
+
5
+ A lightweight, generic React spreadsheet component. React is the only dependency (~5KB gzipped).
6
+
7
+ - **Grid display** — toggleable row numbers and header, per-column widths, drag-to-resize columns, virtualized rows (smooth with tens of thousands of rows)
8
+ - **Cell types** — text / number (normalizes full-width digits and commas) / select (dropdown backed by master data, stores codes while displaying labels) / date (calendar input, normalizes pasted dates in common Japanese formats)
9
+ - **Cell editing** — start editing by double-click, F2, or just typing. **Full IME support**: with a Japanese IME on, pressing "A" opens the editor and types 「あ」 right into the cell
10
+ - **Range selection** — mouse drag, extend with Shift+click / Shift+arrows, add multiple ranges with Ctrl(⌘)+click. Click row/column headers to select whole rows/columns, the top-left corner to select all
11
+ - **Copy & paste** — Ctrl(⌘)+C / X / V. TSV format interoperable with Excel and Google Sheets (handles cells containing newlines, tabs and quotes; tiles single-cell paste across a selection)
12
+
13
+ ## Installation
14
+
15
+ ```sh
16
+ npm install react-masume-grid
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```tsx
22
+ import { useState } from 'react';
23
+ import { MasumeGrid } from 'react-masume-grid';
24
+ // CSS loads automatically when you import the library.
25
+ // Depending on your bundler you may need: import 'react-masume-grid/styles.css';
26
+
27
+ function App() {
28
+ const [data, setData] = useState<string[][]>([
29
+ ['Apple', '100', 'Fruit'],
30
+ ['Carrot', '80', 'Vegetable'],
31
+ ]);
32
+
33
+ return (
34
+ <MasumeGrid
35
+ data={data}
36
+ onChange={setData}
37
+ columns={[
38
+ { title: 'Name', width: 160 },
39
+ { title: 'Price', width: 80 },
40
+ { title: 'Category', width: 120, readOnly: true },
41
+ ]}
42
+ showRowNumbers
43
+ style={{ height: 400 }}
44
+ />
45
+ );
46
+ }
47
+ ```
48
+
49
+ When `columns` is omitted, the column count is derived from `data` and headers show spreadsheet-style letters (A, B, C, …).
50
+
51
+ ## Props
52
+
53
+ | Prop | Type | Default | Description |
54
+ | --- | --- | --- | --- |
55
+ | `data` | `string[][]` | (required) | Grid contents. Rows may be ragged |
56
+ | `columns` | `ColumnDef[]` | — | Array of `{ title?, width?, readOnly?, resizable?, type?, options?, strict? }`. When omitted, the column count is derived from data |
57
+ | `onChange` | `(next: string[][]) => void` | — | Called with a new 2D array on every edit / paste / delete |
58
+ | `onCellChange` | `(row, col, value) => void` | — | Called once per changed cell. Use instead of (or with) `onChange` |
59
+ | `onSelectionChange` | `(ranges: NormalizedRange[]) => void` | — | Called when the selection changes (array of `{top,left,bottom,right}`) |
60
+ | `onColumnResize` | `(col, width) => void` | — | Called when a column resize drag finishes (final width in px) |
61
+ | `showRowNumbers` | `boolean` | `true` | Show the row-number column |
62
+ | `showHeader` | `boolean` | `true` | Show the header row |
63
+ | `readOnly` | `boolean` | `false` | Disallow editing (selection & copy still work) |
64
+ | `resizableColumns` | `boolean` | `true` | Resize columns by dragging header edges. Override per column with `ColumnDef.resizable` (requires `showHeader`) |
65
+ | `rowHeight` | `number` | `28` | Row height (px) |
66
+ | `headerHeight` | `number` | `28` | Header height (px) |
67
+ | `defaultColumnWidth` | `number` | `120` | Width of columns without an explicit width (px) |
68
+ | `rowNumberWidth` | `number` | `48` | Width of the row-number column (px) |
69
+ | `className` / `style` | — | — | Applied to the root element. Set the height via `style` or CSS (default 420px) |
70
+
71
+ The data is fully **controlled**: the grid never changes unless you implement `onChange`.
72
+
73
+ Column widths are the one uncontrolled exception — widths set by dragging are kept inside the component (taking precedence over `ColumnDef.width`). Persist them via `onColumnResize` if needed.
74
+
75
+ ## Cell types
76
+
77
+ Set `ColumnDef.type` to choose a cell type per column. **Data stays plain strings**; the type controls the editor UI, input normalization and display (this keeps clipboard interop simple).
78
+
79
+ ```tsx
80
+ const columns: ColumnDef[] = [
81
+ { title: 'Product' }, // text (default)
82
+ { title: 'Price', type: 'number' },
83
+ { title: 'Category', type: 'select', options: [
84
+ { value: 'C01', label: 'Fruit' }, // stores the code, displays the label
85
+ { value: 'C02', label: 'Produce' },
86
+ ]},
87
+ { title: 'Status', type: 'select', options: ['In stock', 'Backorder'] }, // plain strings work too
88
+ { title: 'Arrival', type: 'date' },
89
+ ];
90
+ ```
91
+
92
+ | Type | Editor | Behavior |
93
+ | --- | --- | --- |
94
+ | `text` | Text (IME-aware) | Default; free text |
95
+ | `number` | Text (IME-aware) | Right-aligned. On commit, full-width digits are converted and thousands separators removed. Non-numeric input is **rejected** (the cell keeps its old value) |
96
+ | `select` | Filtering dropdown | ↑↓ to move, Enter/click to commit, type to filter. Alt+↓ also opens it. `options` accepts `string` or `{value, label}` (stores value, displays label). Values outside the options are rejected by default (`strict: false` allows free input) |
97
+ | `date` | Native date picker | Stored as `YYYY-MM-DD`. Pasted text such as `2026/7/6`, `2026年7月6日`, `20260706` and full-width digits is normalized. Invalid dates are rejected. Alt+↓ opens the calendar |
98
+
99
+ Normalization and validation apply to **both edit commits and paste**. Cells with invalid values are skipped and keep their old value. The normalizers are exported as `normalizeNumberInput` / `normalizeDateInput` for reuse in your own validation.
100
+
101
+ ## Keyboard
102
+
103
+ | Key | Action |
104
+ | --- | --- |
105
+ | Arrows / Tab / Enter | Move between cells (Shift reverses / extends the range) |
106
+ | PageUp / PageDown | Move by a page |
107
+ | Home / End | Start / end of row (Ctrl+Home/End: first / last cell) |
108
+ | Any printable key | Start editing with that character (IME-aware) |
109
+ | F2 / double-click | Start editing, keeping the current value |
110
+ | Enter / Tab | Commit and move; Esc cancels; Alt+Enter inserts a newline in the cell |
111
+ | Delete / Backspace | Clear selected cells |
112
+ | Ctrl(⌘)+A | Select all |
113
+ | Ctrl(⌘)+C / X / V | Copy / cut / paste |
114
+
115
+ ## Styling
116
+
117
+ Override CSS variables to theme the grid.
118
+
119
+ ```css
120
+ .my-grid {
121
+ --masume-grid-accent: #0f9d58;
122
+ --masume-grid-sel-bg: rgba(15, 157, 88, 0.12);
123
+ --masume-grid-header-bg: #f0f4f1;
124
+ }
125
+ ```
126
+
127
+ See the top of [src/masume-grid.css](src/masume-grid.css) for the full list of variables.
128
+
129
+ ## How IME support works
130
+
131
+ The grid keeps an invisible, always-focused `<textarea>` positioned over the active cell (the same technique as Google Sheets). Editing starts on `compositionstart`, so the IME candidate window appears at the cell, and the Enter that confirms a composition is never misinterpreted as cell navigation (including Safari's different event ordering).
132
+
133
+ ## Development
134
+
135
+ ```sh
136
+ npm install
137
+ npm run dev # demo app (http://localhost:5173)
138
+ npm test # unit tests (vitest)
139
+ npm run typecheck # type check
140
+ npm run build # library build into dist/ (ESM + CJS + d.ts + CSS)
141
+ ```
142
+
143
+ ## Limitations (current version)
144
+
145
+ - Internal data is always strings (numbers/dates included; display formatting such as thousands separators is future work)
146
+ - Columns are not virtualized — mind performance beyond a few hundred columns
147
+ - No undo / redo (the `onChange`-based design lets the host app manage history)
148
+ - No merged cells, formulas, or double-click auto-fit for column widths
149
+
150
+ ## License
151
+
152
+ MIT
@@ -0,0 +1,2 @@
1
+ import { MasumeGridProps } from './types';
2
+ export declare function MasumeGrid({ data, columns, onChange, onCellChange, onSelectionChange, onColumnResize, showRowNumbers, showHeader, readOnly, resizableColumns, rowHeight, headerHeight, defaultColumnWidth, rowNumberWidth, className, style, }: MasumeGridProps): import("react").JSX.Element;
@@ -0,0 +1,3 @@
1
+ export { MasumeGrid } from './MasumeGrid';
2
+ export type { CellPos, CellValue, ColumnDef, ColumnType, MasumeGridProps, NormalizedRange, SelectOption, } from './types';
3
+ export { normalizeDateInput, normalizeNumberInput, toHalfWidth } from './utils';
@@ -0,0 +1,6 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const M=require("react/jsx-runtime"),i=require("react");function A(l,c,p){return Math.min(Math.max(l,c),p)}function ft(l){let c="",p=l;for(;c=String.fromCharCode(65+p%26)+c,p=Math.floor(p/26)-1,!(p<0););return c}function dt(l){const c=new Array(l.length+1);c[0]=0;for(let p=0;p<l.length;p++)c[p+1]=c[p]+l[p];return c}function pt(l,c,p){const v=Math.max(0,c-1),E=Math.max(0,p-1),y=l.anchor,w=l.focus;return{top:A(Math.min(y.row,w.row),0,v),bottom:A(Math.max(y.row,w.row),0,v),left:A(Math.min(y.col,w.col),0,E),right:A(Math.max(y.col,w.col),0,E)}}function pe(l){return l.replace(/[0-9a-zA-Z]/g,c=>String.fromCharCode(c.charCodeAt(0)-65248)).replace(/./g,".").replace(/[-ー−]/g,"-").replace(/+/g,"+").replace(/,/g,",").replace(///g,"/").replace(/ /g," ")}function Ve(l){const c=pe(l).trim().replace(/,/g,"");return c===""?"":/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(c)?c:null}function de(l){const c=pe(l).trim();if(c==="")return"";const p=/^(\d{4})[/\-.年](\d{1,2})[/\-.月](\d{1,2})日?$/.exec(c)??/^(\d{4})(\d{2})(\d{2})$/.exec(c);if(!p)return null;const v=Number(p[1]),E=Number(p[2]),y=Number(p[3]),w=new Date(v,E-1,y);if(w.getFullYear()!==v||w.getMonth()!==E-1||w.getDate()!==y)return null;const U=q=>String(q).padStart(2,"0");return`${v}-${U(E)}-${U(y)}`}function mt(l){return/[\t\n\r"]/.test(l)?'"'+l.replace(/"/g,'""')+'"':l}function ht(l){return l.map(c=>c.map(mt).join(" ")).join(`
2
+ `)}function wt(l){l.endsWith(`\r
3
+ `)?l=l.slice(0,-2):(l.endsWith(`
4
+ `)||l.endsWith("\r"))&&(l=l.slice(0,-1));const c=[];let p=[],v="",E=!1;for(let y=0;y<l.length;y++){const w=l[y];E?w==='"'?l[y+1]==='"'?(v+='"',y++):E=!1:v+=w:w==='"'&&v===""?E=!0:w===" "?(p.push(v),v=""):w===`
5
+ `||w==="\r"?(w==="\r"&&l[y+1]===`
6
+ `&&y++,p.push(v),c.push(p),p=[],v=""):v+=w}return p.push(v),c.push(p),c}const ze=4,gt=24;function vt({data:l,columns:c,onChange:p,onCellChange:v,onSelectionChange:E,onColumnResize:y,showRowNumbers:w=!0,showHeader:U=!0,readOnly:q=!1,resizableColumns:Oe=!0,rowHeight:b=28,headerHeight:Pe=28,defaultColumnWidth:me=120,rowNumberWidth:Be=48,className:he,style:Fe}){var Ie,We;const g=l.length,m=i.useMemo(()=>c?c.length:l.reduce((e,t)=>Math.max(e,t.length),0),[c,l]),[we,ge]=i.useState({}),[ve,ye]=i.useState(null),j=i.useMemo(()=>Array.from({length:m},(e,t)=>{var r;return we[t]??((r=c==null?void 0:c[t])==null?void 0:r.width)??me}),[c,m,me,we]),z=i.useMemo(()=>dt(j),[j]),x=w?Be:0,k=U?Pe:0,se=x+(z[m]??0),$e=g*b,[Q,L]=i.useState([{anchor:{row:0,col:0},focus:{row:0,col:0}}]),[h,le]=i.useState(null),[B,H]=i.useState(""),[be,Ue]=i.useState(0),[xe,De]=i.useState(0),[J,ae]=i.useState(null),[X,Z]=i.useState(0),V=i.useRef(null),W=i.useRef(null),F=i.useRef(null),ee=i.useRef(null),te=i.useRef(!1),Me=i.useRef(-1),O=i.useRef(h);O.current=h;const Ee=i.useRef(B);Ee.current=B;const re=Q[Q.length-1],u={row:A(re.anchor.row,0,Math.max(0,g-1)),col:A(re.anchor.col,0,Math.max(0,m-1))},K=i.useMemo(()=>Q.map(e=>pt(e,g,m)),[Q,g,m]);i.useEffect(()=>{E==null||E(K)},[K]),i.useLayoutEffect(()=>{const e=V.current;if(!e||(De(e.clientHeight),typeof ResizeObserver>"u"))return;const t=new ResizeObserver(()=>De(e.clientHeight));return t.observe(e),()=>t.disconnect()},[]);const P=i.useCallback(e=>{var t;return!q&&!((t=c==null?void 0:c[e])!=null&&t.readOnly)},[q,c]),C=i.useCallback(e=>{var t;return((t=c==null?void 0:c[e])==null?void 0:t.type)??"text"},[c]),$=i.useMemo(()=>{const e=new Map;return c==null||c.forEach((t,r)=>{t.type==="select"&&e.set(r,(t.options??[]).map(o=>typeof o=="string"?{value:o,label:o}:{value:o.value,label:o.label??o.value}))}),e},[c]),ie=i.useMemo(()=>{const e=new Map;for(const[t,r]of $)e.set(t,new Map(r.map(o=>[o.value,o.label])));return e},[$]),ke=i.useCallback((e,t)=>{var r;switch(C(e)){case"number":return Ve(t);case"date":return de(t);case"select":{if(t==="")return"";const o=$.get(e);if(o!=null&&o.some(a=>a.value===t))return t;const n=o==null?void 0:o.find(a=>a.label===t);return n?n.value:((r=c==null?void 0:c[e])==null?void 0:r.strict)===!1?t:null}default:return t}},[C,$,c]),oe=i.useCallback(e=>{var r;const t=[];for(const o of e){if(!P(o.col))continue;const n=ke(o.col,o.value);n!==null&&(((r=l[o.row])==null?void 0:r[o.col])??"")!==n&&t.push(n===o.value?o:{...o,value:n})}if(t.length!==0){if(v)for(const o of t)v(o.row,o.col,o.value);if(p){const o=l.slice(),n=new Set;for(const a of t){n.has(a.row)||(o[a.row]=(o[a.row]??[]).slice(),n.add(a.row));const s=o[a.row];for(;s.length<a.col;)s.push("");s[a.col]=a.value}p(o)}}},[P,ke,l,p,v]),T=i.useCallback((e=!0,t)=>{var o,n;const r=O.current;if(r){if(e&&!(C(r.col)==="date"&&((o=F.current)==null?void 0:o.validity.badInput)===!0)){const s=t??Ee.current,d=((n=l[r.row])==null?void 0:n[r.col])??"";s!==d&&oe([{row:r.row,col:r.col,value:s}])}le(null),H(""),ae(null),te.current=!1}},[C,l,oe]),_=i.useCallback(e=>{const t=V.current;if(!t||m===0)return;const r=k+e.row*b,o=r+b,n=x+z[e.col],a=n+j[e.col];r-k<t.scrollTop?t.scrollTop=r-k:o>t.scrollTop+t.clientHeight&&(t.scrollTop=o-t.clientHeight),n-x<t.scrollLeft?t.scrollLeft=n-x:a>t.scrollLeft+t.clientWidth&&(t.scrollLeft=a-t.clientWidth)},[m,k,b,x,z,j]),G=i.useCallback((e,t)=>{var a,s;const r=t??u;if(g===0||m===0||!P(r.col))return;const o=C(r.col);let n=e==="edit"?((a=l[r.row])==null?void 0:a[r.col])??"":"";o==="date"&&(n=de(n)??""),o==="select"&&(n=((s=ie.get(r.col))==null?void 0:s.get(n))??n),le({row:r.row,col:r.col,mode:e}),H(n),ae(null),_(r),e==="edit"&&o!=="date"&&requestAnimationFrame(()=>{var d;(d=W.current)==null||d.setSelectionRange(n.length,n.length)})},[u.row,u.col,g,m,P,C,ie,l,_]),Ce=i.useRef(u);Ce.current=u;const Re=i.useRef({rowNumW:x,headerH:k});i.useLayoutEffect(()=>{const e=Re.current;e.rowNumW===x&&e.headerH===k||(Re.current={rowNumW:x,headerH:k},_(Ce.current))},[x,k,_]);const Y=h?C(h.col):null,I=i.useMemo(()=>{if(!h||Y!=="select")return null;const e=$.get(h.col)??[];if(J===null)return e;const t=J.trim().toLowerCase();return t===""?e:e.filter(r=>r.label.toLowerCase().includes(t)||r.value.toLowerCase().includes(t))},[h,Y,$,J]);i.useEffect(()=>{var t;if(!h||!I)return;let e=I.findIndex(r=>r.value===B||r.label===B);if(e<0&&J===null){const r=((t=l[h.row])==null?void 0:t[h.col])??"";e=I.findIndex(o=>o.value===r)}Z(Math.max(0,e))},[I]),i.useEffect(()=>{var t;const e=(t=ee.current)==null?void 0:t.children[X];e==null||e.scrollIntoView({block:"nearest"})},[X]),i.useEffect(()=>{var e;h&&Y==="date"&&((e=F.current)==null||e.focus({preventScroll:!0}))},[h,Y]);const ne=(e,t)=>{const r=e.slice(),o=r[r.length-1];return r[r.length-1]={anchor:o.anchor,focus:t},r},D=(e,t,r)=>{if(g===0||m===0)return;const o={row:A(e,0,g-1),col:A(t,0,m-1)};r!=null&&r.extend?L(n=>ne(n,o)):L([{anchor:o,focus:o}]),_(o)},Se=()=>{g===0||m===0||L([{anchor:{row:0,col:0},focus:{row:g-1,col:m-1}}])},ue=()=>{var r;const e=K[K.length-1],t=[];for(let o=e.top;o<=e.bottom;o++){const n=[];for(let a=e.left;a<=e.right;a++)n.push(((r=l[o])==null?void 0:r[a])??"");t.push(n)}return ht(t)},Ne=async()=>{var t;if(g===0||m===0)return;const e=ue();try{await navigator.clipboard.writeText(e)}catch{const r=document.createElement("textarea");r.value=e,r.style.position="fixed",r.style.opacity="0",document.body.appendChild(r),r.select();try{document.execCommand("copy")}finally{r.remove(),(t=W.current)==null||t.focus({preventScroll:!0})}}},fe=()=>{var r;const e=[],t=new Set;for(const o of K)for(let n=o.top;n<=o.bottom;n++)for(let a=o.left;a<=o.right;a++){const s=n*Math.max(1,m)+a;t.has(s)||(t.add(s),(((r=l[n])==null?void 0:r[a])??"")!==""&&e.push({row:n,col:a,value:""}))}oe(e)},qe=e=>{if(q||g===0||m===0)return;const t=wt(e);if(t.length===0)return;const r=K[K.length-1],o=t.length,n=Math.max(...t.map(R=>R.length));if(n===0)return;const a=r.bottom-r.top+1,s=r.right-r.left+1;let d=o,f=n;(a>o||s>n)&&a%o===0&&s%n===0&&(d=a,f=s),d=Math.min(d,g-r.top),f=Math.min(f,m-r.left);const S=[];for(let R=0;R<d;R++)for(let N=0;N<f;N++)S.push({row:r.top+R,col:r.left+N,value:t[R%o][N%n]??""});oe(S),L([{anchor:{row:r.top,col:r.left},focus:{row:r.top+d-1,col:r.left+f-1}}])},Xe=e=>{if(te.current||e.nativeEvent.isComposing||e.keyCode===229)return;const t=e.shiftKey,r=e.ctrlKey||e.metaKey,o=h;if(o){if((e.key==="Enter"||e.key==="Tab"||e.key==="Escape")&&e.timeStamp-Me.current<80||e.key==="Enter"&&e.altKey)return;if(C(o.col)==="select"&&I){if(e.key==="ArrowDown"){e.preventDefault(),Z(s=>Math.min(s+1,Math.max(0,I.length-1)));return}if(e.key==="ArrowUp"){e.preventDefault(),Z(s=>Math.max(s-1,0));return}if(e.key==="Enter"||e.key==="Tab"){e.preventDefault();const s=I[X];T(!0,s==null?void 0:s.value),e.key==="Enter"?D(u.row+(t?-1:1),u.col):D(u.row,u.col+(t?-1:1));return}}switch(e.key){case"Enter":e.preventDefault(),T(),D(u.row+(t?-1:1),u.col);return;case"Tab":e.preventDefault(),T(),D(u.row,u.col+(t?-1:1));return;case"Escape":e.preventDefault(),T(!1);return;case"ArrowUp":case"ArrowDown":case"ArrowLeft":case"ArrowRight":{if(o.mode!=="replace")return;e.preventDefault(),T();const s=e.key==="ArrowUp"?-1:e.key==="ArrowDown"?1:0,d=e.key==="ArrowLeft"?-1:e.key==="ArrowRight"?1:0;D(u.row+s,u.col+d);return}default:return}}const n=(s,d)=>{if(e.preventDefault(),t){const f={row:A(re.focus.row,0,Math.max(0,g-1)),col:A(re.focus.col,0,Math.max(0,m-1))};D(f.row+s,f.col+d,{extend:!0})}else D(u.row+s,u.col+d)};if(e.altKey&&!r&&e.key==="ArrowDown"){const s=C(u.col);if(s==="select"||s==="date"){e.preventDefault(),G("edit"),s==="date"&&requestAnimationFrame(()=>{var d,f;try{(f=(d=F.current)==null?void 0:d.showPicker)==null||f.call(d)}catch{}});return}}if(r&&!e.altKey){const s=e.key.toLowerCase();if(s==="a"){e.preventDefault(),Se();return}if(s==="c"){e.preventDefault(),Ne();return}if(s==="x"){e.preventDefault(),Ne(),fe();return}if(s==="v")return;if(e.key==="Home"){e.preventDefault(),D(0,0,{extend:t});return}if(e.key==="End"){e.preventDefault(),D(g-1,m-1,{extend:t});return}return}const a=Math.max(1,Math.floor(Math.max(0,xe-k)/b)-1);switch(e.key){case"ArrowUp":n(-1,0);return;case"ArrowDown":n(1,0);return;case"ArrowLeft":n(0,-1);return;case"ArrowRight":n(0,1);return;case"PageUp":n(-a,0);return;case"PageDown":n(a,0);return;case"Tab":e.preventDefault(),D(u.row,u.col+(t?-1:1));return;case"Enter":e.preventDefault(),D(u.row+(t?-1:1),u.col);return;case"F2":e.preventDefault(),G("edit");return;case"Delete":case"Backspace":e.preventDefault(),fe();return;case"Home":e.preventDefault(),D(u.row,0,{extend:t});return;case"End":e.preventDefault(),D(u.row,m-1,{extend:t});return}e.key.length===1&&!r&&!e.altKey&&(C(u.col)==="date"&&e.preventDefault(),G("replace"))},_e=e=>{var t,r,o;switch(e.key){case"Enter":e.preventDefault(),T(),(t=W.current)==null||t.focus({preventScroll:!0}),D(u.row+(e.shiftKey?-1:1),u.col);return;case"Tab":e.preventDefault(),T(),(r=W.current)==null||r.focus({preventScroll:!0}),D(u.row,u.col+(e.shiftKey?-1:1));return;case"Escape":e.preventDefault(),T(!1),(o=W.current)==null||o.focus({preventScroll:!0});return}},Ge=e=>{const t=O.current;if(!(t&&C(t.col)==="date")){if(!t){if(!P(u.col)||C(u.col)==="date")return;le({row:u.row,col:u.col,mode:"replace"})}H(e.target.value),ae(e.target.value)}},Ae=e=>{var t;(t=V.current)!=null&&t.contains(e.relatedTarget)||T(!0)},Ye=()=>{te.current=!0,O.current||G("replace")},Qe=e=>{te.current=!1,Me.current=e.timeStamp},He=e=>{O.current||(e.preventDefault(),e.clipboardData.setData("text/plain",ue()))},Je=e=>{O.current||(e.preventDefault(),e.clipboardData.setData("text/plain",ue()),fe())},Ze=e=>{if(O.current)return;e.preventDefault();const t=e.clipboardData.getData("text/plain");t&&qe(t)},et=(e,t)=>{const r=V.current,o=r.getBoundingClientRect(),n=e-o.left+r.scrollLeft-x,a=t-o.top+r.scrollTop-k,s=A(Math.floor(a/b),0,Math.max(0,g-1));let d=m-1;for(let f=0;f<m;f++)if(n<z[f+1]){d=f;break}return{row:s,col:A(d,0,Math.max(0,m-1))}},tt=()=>{const e=r=>{if(!V.current)return;const o=et(r.clientX,r.clientY);L(n=>ne(n,o))},t=()=>{window.removeEventListener("mousemove",e),window.removeEventListener("mouseup",t)};window.addEventListener("mousemove",e),window.addEventListener("mouseup",t)},Le=(e,t,r)=>{L(t?o=>ne(o,e.focus):r?o=>[...o,e]:[e])},rt=e=>{var d;if(e.button!==0)return;const t=e.target;if(t===W.current||F.current&&F.current.contains(t)||ee.current&&ee.current.contains(t)||t===V.current||(e.preventDefault(),(d=W.current)==null||d.focus({preventScroll:!0}),g===0||m===0))return;T();const r=e.ctrlKey||e.metaKey,o=t.closest("[data-corner]"),n=t.closest("[data-hcol]"),a=t.closest("[data-rownum]"),s=t.closest("[data-row]");if(o){Se();return}if(n){const f=Number(n.dataset.hcol);Le({anchor:{row:0,col:f},focus:{row:g-1,col:f}},e.shiftKey,r);return}if(a){const f=Number(a.dataset.rownum);Le({anchor:{row:f,col:0},focus:{row:f,col:m-1}},e.shiftKey,r);return}if(s){const f={row:Number(s.dataset.row),col:Number(s.dataset.col)};e.shiftKey?L(S=>ne(S,f)):L(r?S=>[...S,{anchor:f,focus:f}]:[{anchor:f,focus:f}]),tt()}},ot=e=>{var t;return((t=c==null?void 0:c[e])==null?void 0:t.resizable)??Oe},nt=(e,t)=>{var f;if(e.button!==0)return;e.preventDefault(),e.stopPropagation(),(f=W.current)==null||f.focus({preventScroll:!0});const r=e.clientX,o=j[t],n=S=>Math.max(gt,o+(S-r));ye(t);const a=document.body.style.cursor;document.body.style.cursor="col-resize";const s=S=>{const R=n(S.clientX);ge(N=>N[t]===R?N:{...N,[t]:R})},d=S=>{window.removeEventListener("mousemove",s),window.removeEventListener("mouseup",d),document.body.style.cursor=a,ye(null);const R=n(S.clientX);ge(N=>N[t]===R?N:{...N,[t]:R}),y==null||y(t,R)};window.addEventListener("mousemove",s),window.addEventListener("mouseup",d)},ct=e=>{const t=e.target.closest("[data-row]");t&&G("edit",{row:Number(t.dataset.row),col:Number(t.dataset.col)})},st=Math.max(0,Math.floor(be/b)-ze),lt=Math.min(g,Math.ceil((be+Math.max(0,xe-k))/b)+ze),at=e=>K.some(t=>e>=t.left&&e<=t.right),it=e=>K.some(t=>e>=t.top&&e<=t.bottom),Te=[];for(let e=st;e<lt;e++){const t=[];w&&t.push(M.jsx("div",{"data-rownum":e,className:"masume-grid-rownum"+(it(e)?" masume-grid-rownum--sel":""),style:{width:x,height:b},children:e+1},"rownum"));for(let r=0;r<m;r++){const o=K.some(f=>e>=f.top&&e<=f.bottom&&r>=f.left&&r<=f.right),n=e===u.row&&r===u.col,a=C(r),s=((Ie=l[e])==null?void 0:Ie[r])??"",d=a==="select"?((We=ie.get(r))==null?void 0:We.get(s))??s:s;t.push(M.jsxs("div",{"data-row":e,"data-col":r,role:"gridcell","aria-selected":o,className:"masume-grid-cell"+(o?" masume-grid-cell--sel":"")+(n?" masume-grid-cell--active":"")+(P(r)?"":" masume-grid-cell--readonly")+(a==="number"?" masume-grid-cell--num":"")+(a==="select"?" masume-grid-cell--select":""),style:{width:j[r],height:b},children:[d,a==="select"&&M.jsx("span",{className:"masume-grid-cell-arrow",children:"▾"})]},r))}Te.push(M.jsx("div",{role:"row",className:"masume-grid-row",style:{top:e*b,width:se,height:b},children:t},e))}const ce=h??u,ut=g>0&&m>0,je=h!==null&&Y==="date",Ke=h!==null&&!je;return M.jsxs("div",{ref:V,role:"grid","aria-rowcount":g,"aria-colcount":m,className:"masume-grid"+(ve!==null?" masume-grid--resizing":"")+(he?" "+he:""),style:Fe,onScroll:e=>Ue(e.target.scrollTop),onMouseDown:rt,onDoubleClick:ct,children:[U&&M.jsxs("div",{className:"masume-grid-head",style:{width:se,height:k},children:[w&&M.jsx("div",{"data-corner":!0,className:"masume-grid-corner",style:{width:x,height:k}}),Array.from({length:m},(e,t)=>{var r;return M.jsxs("div",{"data-hcol":t,className:"masume-grid-hcell"+(at(t)?" masume-grid-hcell--sel":""),style:{width:j[t],height:k},children:[M.jsx("span",{className:"masume-grid-hcell-label",children:((r=c==null?void 0:c[t])==null?void 0:r.title)??ft(t)}),ot(t)&&M.jsx("div",{className:"masume-grid-resize-handle"+(ve===t?" masume-grid-resize-handle--active":""),onMouseDown:o=>nt(o,t),onDoubleClick:o=>o.stopPropagation()})]},t)})]}),M.jsxs("div",{className:"masume-grid-body",style:{width:se,height:$e},children:[Te,ut&&M.jsx("textarea",{ref:W,className:"masume-grid-editor"+(Ke?"":" masume-grid-editor--hidden"),style:{top:ce.row*b,left:x+z[ce.col],width:j[ce.col],height:b},value:Ke?B:"",onChange:Ge,onKeyDown:Xe,onCompositionStart:Ye,onCompositionEnd:Qe,onCopy:He,onCut:Je,onPaste:Ze,onBlur:Ae,readOnly:!h&&!P(u.col),inputMode:C(ce.col)==="number"?"decimal":void 0,wrap:"off",rows:1,spellCheck:!1,autoCapitalize:"off",autoCorrect:"off","aria-label":"cell editor"}),h&&je&&M.jsx("input",{ref:F,type:"date",className:"masume-grid-editor masume-grid-editor--date",style:{top:h.row*b,left:x+z[h.col],width:Math.max(j[h.col],150),height:b},value:B,onChange:e=>H(e.target.value),onKeyDown:_e,onBlur:Ae,"aria-label":"date editor"}),h&&I&&I.length>0&&M.jsx("div",{ref:ee,className:"masume-grid-dropdown",role:"listbox",style:{top:(h.row+1)*b,left:x+z[h.col],minWidth:j[h.col]},children:I.map((e,t)=>M.jsx("div",{role:"option","aria-selected":t===X,className:"masume-grid-option"+(t===X?" masume-grid-option--hi":""),onMouseDown:r=>{r.preventDefault(),r.stopPropagation(),T(!0,e.value)},onMouseEnter:()=>Z(t),children:e.label},`${e.value}\0${t}`))})]})]})}exports.MasumeGrid=vt;exports.normalizeDateInput=de;exports.normalizeNumberInput=Ve;exports.toHalfWidth=pe;
@@ -0,0 +1 @@
1
+ .masume-grid{--masume-grid-border: #d5dae1;--masume-grid-header-bg: #f5f6f8;--masume-grid-header-fg: #55606e;--masume-grid-header-sel-bg: #e2ebf8;--masume-grid-cell-bg: #ffffff;--masume-grid-cell-fg: #1d232b;--masume-grid-readonly-fg: #8a93a0;--masume-grid-sel-bg: rgba(29, 112, 214, .12);--masume-grid-accent: #1d70d6;position:relative;overflow:auto;overflow-anchor:none;height:420px;border:1px solid var(--masume-grid-border);background:var(--masume-grid-cell-bg);color:var(--masume-grid-cell-fg);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Hiragino Sans,Noto Sans JP,Meiryo,sans-serif;font-size:13px;line-height:1.4;user-select:none;-webkit-user-select:none;box-sizing:border-box}.masume-grid *,.masume-grid *:before,.masume-grid *:after{box-sizing:border-box}.masume-grid:focus-within{border-color:var(--masume-grid-accent)}.masume-grid-head{position:sticky;top:0;z-index:3;display:flex;background:var(--masume-grid-header-bg)}.masume-grid-hcell,.masume-grid-corner{flex:none;display:flex;align-items:center;justify-content:center;border-right:1px solid var(--masume-grid-border);border-bottom:1px solid var(--masume-grid-border);background:var(--masume-grid-header-bg);color:var(--masume-grid-header-fg);font-weight:500;cursor:default}.masume-grid-hcell{position:relative}.masume-grid-hcell-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.masume-grid-resize-handle{position:absolute;top:0;right:-5px;width:10px;height:100%;cursor:col-resize;z-index:1}.masume-grid-resize-handle:after{content:"";position:absolute;top:0;bottom:0;left:4px;width:2px;background:transparent}.masume-grid-resize-handle:hover:after,.masume-grid-resize-handle--active:after{background:var(--masume-grid-accent)}.masume-grid--resizing,.masume-grid--resizing .masume-grid-cell,.masume-grid--resizing .masume-grid-hcell{cursor:col-resize}.masume-grid-corner{position:sticky;left:0;z-index:4}.masume-grid-hcell--sel{background:var(--masume-grid-header-sel-bg);color:var(--masume-grid-accent)}.masume-grid-body{position:relative}.masume-grid-row{position:absolute;left:0;display:flex}.masume-grid-rownum{position:sticky;left:0;z-index:1;flex:none;display:flex;align-items:center;justify-content:center;border-right:1px solid var(--masume-grid-border);border-bottom:1px solid var(--masume-grid-border);background:var(--masume-grid-header-bg);color:var(--masume-grid-header-fg);cursor:default}.masume-grid-rownum--sel{background:var(--masume-grid-header-sel-bg);color:var(--masume-grid-accent)}.masume-grid-cell{flex:none;display:flex;align-items:center;padding:0 6px;border-right:1px solid var(--masume-grid-border);border-bottom:1px solid var(--masume-grid-border);background:var(--masume-grid-cell-bg);overflow:hidden;white-space:pre;cursor:cell}.masume-grid-cell--readonly{color:var(--masume-grid-readonly-fg)}.masume-grid-cell--sel{background:var(--masume-grid-sel-bg)}.masume-grid-cell--active{background:var(--masume-grid-cell-bg);outline:2px solid var(--masume-grid-accent);outline-offset:-2px}.masume-grid-editor{position:absolute;z-index:2;margin:0;padding:2px 5px;border:2px solid var(--masume-grid-accent);border-radius:0;background:var(--masume-grid-cell-bg);color:inherit;font:inherit;resize:none;overflow:hidden;outline:none}.masume-grid-editor--hidden{opacity:0;pointer-events:none;border:none;padding:0}.masume-grid-editor--date{padding:0 4px}.masume-grid-cell--num{justify-content:flex-end;font-variant-numeric:tabular-nums}.masume-grid-cell--select{position:relative;padding-right:18px}.masume-grid-cell-arrow{position:absolute;right:4px;color:var(--masume-grid-readonly-fg);font-size:10px;pointer-events:none}.masume-grid-dropdown{position:absolute;z-index:5;max-height:200px;overflow-y:auto;background:var(--masume-grid-cell-bg);border:1px solid var(--masume-grid-border);box-shadow:0 4px 12px #00000024}.masume-grid-option{padding:4px 8px;white-space:nowrap;cursor:pointer}.masume-grid-option--hi{background:var(--masume-grid-sel-bg);color:var(--masume-grid-accent)}