diff --git a/README.en.md b/README.en.md index 73606830..35526bf7 100644 --- a/README.en.md +++ b/README.en.md @@ -1,41 +1,11 @@ -# 📊 ExcelToGo — describe what you want, get a suggested Excel formula +# ExcelToGo -**Language:** [ไทย](README.md) · English - -### ▶ [Try it — nothing to install](https://excel-to-go.vercel.app) +**A browser-based spreadsheet that suggests Excel formulas from a plain-language description and processes everything in the browser** -**Status (Sep 2026):** closing a last set of fixes before new features pause ([the list](https://github.com/SuruchBoss/ExcelToGo/issues?q=label%3Astatus%3Asprint-final)). After that the app stays free and open as it is, and [bug reports](https://github.com/SuruchBoss/ExcelToGo/issues) and security ([SECURITY.md](SECURITY.md)) are still looked after. +**Language:** [ไทย](README.md) · English -Runs in your browser; your data stays on your machine. -This site is the real app — free, no sign-up, not a trial — and you can [connect your own API from the browser](#from-your-browser-restcsv--works-on-this-site) -right here, with the URL, the header and the data never passing through our server. - -> **Type "total sales for the northern branch" and get a suggested Excel formula back**, with a sentence -> saying what it does — one click puts it in the cell. No remembering which argument SUMIF takes -> first. Or skip the typing: **pick from 37 ready-made formulas** and drag across the cells instead -> of typing addresses. -> -> **Try it without a key: the app guesses a formula from keywords and always says it's a guess — check -> before you use it.** (That way the question, the range address and the column headers go to this -> site's server for the guess.) Add your own Anthropic API key for real AI answers from Claude — the key stays in -> your browser, and the question goes straight to Anthropic, never through this app's server. -> -> All of that happens on a `.xlsx` that **still looks like itself when it opens** (colour bands, -> merged cells, borders, row heights), computed by a **hand-written formula engine** (no -> third-party library), entirely in your browser — the file never leaves your machine (except that -> asking the AI sends the question and its column headers: [where they go](#-ask-ai-for-a-formula)), and there -> is no account to create. -> -> Four things build on that: **[your own API feeding the cells](#-live-data-from-an-api--csv-prototype)**, with no script to write — -> [straight from your browser on this site](#from-your-browser-restcsv--works-on-this-site), including APIs behind a VPN or in the office, or a database -> when you run the app yourself — keeping cells current on its own (following paginated APIs and backing off when rate-limited), -> **[imported files keeping their look](#-it-looks-like-the-file-you-opened)** (colour bands, large type, -> borders, merged cells), **[templates read straight out of an Excel file](#-templates-from-an-excel-file)** -> that already know which cells are yours to fill in, and **[Pivot summaries](#-pivot-summarise-a-range)** -> that group a range and write the result out as a new sheet. -> -> Charts exported with the `.xlsx` are **real charts you can keep editing in Excel**, not pictures — -> the OOXML chart part is written by hand, because ExcelJS cannot write one. +[Open the app](https://excel-to-go.vercel.app) · [Data connection guide](https://excel-to-go.vercel.app/guide) · +[Report an issue](https://github.com/SuruchBoss/ExcelToGo/issues) · [Security policy](SECURITY.md)

Next.js @@ -48,101 +18,107 @@ right here, with the URL, the header and the data never passing through our serv CI

-Describe what you want in plain Thai or English and get a suggested Excel formula back, with a sentence -explaining it; one click puts it in the cell. Without a key that is a local keyword guess — free, marked as a -guess every time, to be checked before use — and pasting your own Anthropic API key sends the question to the -real Claude, from your browser straight to Anthropic, never through this app's server. - -Beyond the assistant, a Next.js web app that turns an Excel-style grid into a friendlier UI: drag-and-drop ready-made formulas instead -of memorizing syntax, an AI assistant that suggests formulas from a natural-language question (Thai or English), -and a hand-written formula engine (tokenizer → parser → evaluator, no third-party formula library) supporting -cell/range references, relative & structural reference adjustment, circular-reference detection, multi-sheet -workbooks, named ranges (in Thai, substituted at compile time so the dependency graph stays honest), rules on -what a cell will accept that refuse a value rather than flag it afterwards, conditional formatting that -re-colours cells from their current values, pivot summaries over a selected range, live data from a REST/CSV -endpoint fetched by your own browser (on the public site, nothing to install, nothing through its server) or, -self-hosted, straight from PostgreSQL/MySQL — one saved read-only query, and nobody downstream ever sees SQL — -and full-fidelity Excel/PDF export, where a chart exported to `.xlsx` is a real, editable chart -bound to its cells, because the OOXML chart parts are written by hand (ExcelJS writes none). Plus optional -bring-your-own-backend cloud save and live co-editing over it — presence, last-writer-wins with the loser told, and -an undo that does not erase the other person's work. Bilingual UI (Thai/English), 2017 automated tests. +> [!NOTE] +> **Project status (October 2026):** the final set of work before the development pause +> ([label `status:sprint-final`](https://github.com/SuruchBoss/ExcelToGo/issues?q=label%3Astatus%3Asprint-final)) +> was completed on 5 October 2026, and new feature development is now paused. The app remains free to use, +> and the project continues to accept [issue reports](https://github.com/SuruchBoss/ExcelToGo/issues) and to +> handle security matters as described in [SECURITY.md](SECURITY.md). + +## Overview + +ExcelToGo is a browser-based spreadsheet for people who use Excel every day but do not write formulas as part +of their job. The user describes what they need in Thai or English, for example _"total sales for the northern +branch"_, and receives a suggested Excel formula with an explanation before choosing whether to insert it. +Alternatively, the user picks one of 37 ready-made formulas and selects the ranges directly in the grid instead +of typing cell addresses. + +All formulas are computed by an engine written for this project, without a third-party formula library. +Imported `.xlsx` files keep their appearance, and exported files keep their formulas as formulas. The site +linked above is the production application, not a trial; it is free to use and requires no account. + +## Key capabilities + +- **Formula assistant** — [ask in Thai or English](#ask-ai-for-a-formula) and receive a formula with an + explanation. Without an API key, the app offers a keyword-based guess and labels it as a guess to be checked + before use. With the user's own [Anthropic API key](#bring-your-own-api-key-byok), the question is sent from + the browser directly to Claude. +- **Drag-and-drop formulas** — [choose a formula from the list](#drag-and-drop-formulas), fill in one field at a + time, and select ranges from the grid without remembering the argument order. +- **Hand-written formula engine** — [tokenizer → parser → evaluator](#formula-engine), with cell and range + references, reference adjustment on copy and on row or column insertion, circular-reference detection, + multiple sheets and [named ranges in Thai](#named-ranges). +- **Excel import and export** — [imported files keep their appearance](#it-looks-like-the-file-you-opened) + (colour bands, merged cells, borders, row heights), and files can be [exported](#export) to `.xlsx`, CSV or + PDF; charts in an exported `.xlsx` are native charts that remain editable in Excel. +- **Templates from Excel files** — [a protected workbook is read as a template](#templates-from-an-excel-file) + that knows which cells the user is expected to fill in. +- **Live data from APIs** — [connect a REST/CSV API from the browser](#from-your-browser-restcsv--works-on-this-site) + on this site, including APIs behind a VPN or on an office network, or connect + [PostgreSQL / MySQL](#straight-into-a-database-postgresql--mysql) when self-hosting. Cells refresh on a + schedule, with support for paginated APIs and back-off when rate-limited. +- **Pivot summaries** — [group a range](#pivot-summarise-a-range) and write the result to a new sheet, with a + notice when the source data changes. +- **Mobile and bilingual use** — [touch support](#works-on-a-phone) and a + [Thai and English interface](#bilingual-thai--english). +- **Cloud save and co-editing (optional)** — [on a backend the user operates](#cloud-save-bring-your-own-backend). + +## Data handling + +Sheets, imported files and exported files are processed entirely in the user's browser. Data leaves the device +only in the following cases. + +| Case | What is sent | Destination | +|---|---|---| +| Asking the AI without an API key | The question, the range address, the column headers and any values the user typed into the question ([details](#ask-ai-for-a-formula)) | This site's server | +| Asking the AI with the user's own API key | The same information | Anthropic, directly from the browser | +| Live data connected from the browser | Requests to the API the user configured | That API directly, not through this site's server | +| Usage counter (when enabled by the operator) | Event names from a fixed list, with no cell contents and no identifiers ([details](#a-usage-count-that-provably-cannot-identify-anyone)) | This site's server | +| Crash report (when configured by the operator) | The error message and a stack trace with keys, tokens, email addresses and Thai text removed | An endpoint chosen by the operator | +| Cloud save (when enabled) | The workbook | A backend the user configures | --- -## ⏱️ Try it in 60 seconds +## Try it in 60 seconds

The three steps: change a price and the totals move, build a pivot, and the summary flags its source as stale

-

The three steps below, recorded from the running app — no edits

+

The three steps below, recorded from the running application without editing

-If you only have a minute — [**open the app**](https://excel-to-go.vercel.app/app), press **Try it with -sample data** on the green bar, and do these three things in order. Nothing to install, no sign-up. +[**Open the app**](https://excel-to-go.vercel.app/app), press **Try it with sample data** on the green bar, and +follow the three steps below. No installation or account is required. -The app opens on a **blank sheet**, because the first thing a person does is their own work, and a grid -already full of somebody else's coffee prices read as data left behind (it used to open on the sample, -and the first person to try it asked why the app had data left over). So the nine sample rows are one -press away, and every total in them is a **formula**, not a number. Undo takes the sample away again, and -**Start from a blank sheet** does too. +The application opens on a **blank sheet**, since most users start with their own work, and a sheet that already +contained sample data was taken for leftover data. The nine sample rows are therefore one press away, and every +total in them is a **formula**, not a number. The sample can be removed again with undo or with +**Start from a blank sheet**. -| | Do this | What you'll see | +| | Step | Result | |---|---|---| | **1** | Click **C2** (the latte's price), type a different number, press Enter | The **Total** column and the **Grand total** row move with it — every total is a real formula, not a number somebody typed once | | **2** | Select **A1:E10**, press **Summarise (Pivot)** → group by *Category* → value *Total* → **Build summary sheet** | A new sheet summarising by category. Go back and change a price, then watch the **"the source data has changed"** notice appear on the summary | | **3** | Select **A1:E10** again and press **Export Excel** | Open it in real Excel — the formulas are still formulas, not baked-in values. Add a chart first and it exports as **a real chart you can keep editing** | -Want the harder parts: [embedding a Thai font in the PDF, with stacked tone marks](#-export) · -[hand-written OOXML chart parts](#-charts-from-the-sheet) · [an accessibility gate in CI](#-testing) · -[the formula engine](#-formula-engine) +Related technical detail: [embedding a Thai font in the PDF, with stacked tone marks](#export) · +[hand-written OOXML chart parts](#charts-from-the-sheet) · [the accessibility gate in CI](#testing) · +[the formula engine](#formula-engine) -> Every section explains the decision behind it, including **what it still can't do** — see -> [What's next](#-whats-next). +> Each section records the reasoning behind its design decisions, including **what is not yet supported**, +> which is collected in [What's next](#whats-next). + +--- -## 📸 Screenshots +## Screenshots

Main screen — the data grid with the drag-and-drop formula panel on the right

-> The rest of the screenshots live in [**Features**](#-features) below, each one next to the feature it shows. -> Every shot is from a production build, not a mockup. +> Further screenshots appear under [**Features**](#features), each next to the feature it illustrates. +> All screenshots are taken from a production build, not mock-ups. --- ---- - -### 🧪 What 2017 passing tests could not catch - -Every test of the assistant **mocks the model** — it returns what I imagined it would. Put a real -API key behind it, ask fourteen ordinary questions, and **six answers used functions this engine -does not have** (`TEXTJOIN`, `FIND`, `RANK.EQ`, `SUMPRODUCT`, `CEILING`, `CHAR`). All valid Excel; -all `#NAME?` in the cell, right after pressing a button labelled "insert". - -Then **the first fix made it worse.** The rule started as "give the closest formula the list -allows", so _"join all the names into one line"_ came back as `=SUM(A2:A20)` — `0` in the cell, no -error, nothing to notice. **A visible `#NAME?` traded for an invisible wrong number.** - -**And it happened again, in a different place.** With every gate green — 2017 tests, `axe` clean on -both pages at two widths — an hour of clicking through the public build the way a first-time visitor -would found three things no gate can see: - -- Half-insert a formula from the palette and **all six sidebar buttons went dead.** They still set - the mode in the store; a `pending ? … : mode` render just outranked it. Escape did nothing either, - so the press arrived later, attached to whichever click finally cancelled the formula. -- The keyword matcher — which on the public site, with no key set, is not a fallback but the *only* thing anyone sees - — answered _"add up all the sales"_ with `=SUM(E2)`, the total of one cell, and _"join the product - name and the category"_ with `=SUM(E2)` again. The system prompt has forbidden the model from - substituting like that since the fix above. Nothing had ever told the matcher. -- `axe` passed the grid at four viewport/page combinations while it was, to a screen reader, an - ordinary table you could not drive: no `role="grid"`, no `aria-selected`, no `scope` on the - headers, every cell tabbable, and **the browser's focus stayed on A1 while Ctrl+↓ moved the cursor - to A10** — the sheet moved in silence. axe was right to pass. A `` with `
` *is* a valid - table. It just was not what this is. - -All four are fixed. The lesson each time is the same one: a green suite says the code does what the -tests say, and nothing whatever about whether that is the right thing. - -→ [The whole story, and the fix](#-ask-ai-for-a-formula) · repeatable with `npm run check:ai` - -## 💼 Problems it solves +## Problems it solves A summary for whoever decides whether to use it — told from **the problems a team already pays for**, not from a feature list. Each one is solved by several features working together, and every feature @@ -162,9 +138,9 @@ the outcome under a double rule like a total. **Solved by** -1. **[Ask the AI in plain Thai or English](#-ask-ai-for-a-formula)** — Type “total sales for the North branch only” and get a formula with an explanation before you decide to use it. Needs [your own API key](#-bring-your-own-api-key-byok) — without one, the app guesses from keywords and only manages basic formulas -2. **[37 ready-made formulas](#-drag-and-drop-formulas)** — Rather not type? Pick from the list, fill it in field by field, and press the crosshair to drag-select the range on the real sheet instead of typing cell addresses -3. **[Name ranges, in Thai if you like](#-named-ranges)** — `=SUM(ยอดขาย)` instead of `=SUM(B2:B500)` — whoever inherits the file can read the formula without chasing the person who wrote it +1. **[Ask the AI in plain Thai or English](#ask-ai-for-a-formula)** — Type “total sales for the North branch only” and get a formula with an explanation before you decide to use it. Needs [your own API key](#bring-your-own-api-key-byok) — without one, the app guesses from keywords and only manages basic formulas +2. **[37 ready-made formulas](#drag-and-drop-formulas)** — Rather not type? Pick from the list, fill it in field by field, and press the crosshair to drag-select the range on the real sheet instead of typing cell addresses +3. **[Name ranges, in Thai if you like](#named-ranges)** — `=SUM(ยอดขาย)` instead of `=SUM(B2:B500)` — whoever inherits the file can read the formula without chasing the person who wrote it > **Outcome:** People in the team build their own reports the same day, and the file still makes sense after its author has moved on @@ -177,10 +153,10 @@ the outcome under a double rule like a total. **Solved by** -1. **[Restrict what can be entered](#-data-validation)** — A cell becomes a dropdown or accepts only numbers in a range. Anything outside the rule is **not saved** — refused, not stored and flagged later -2. **[See which cells a formula reads](#-see-what-a-formula-is-about)** — Click the total and the cells it reads light up on the sheet — a range one column too wide is visible at a glance -3. **[Pick ranges by dragging](#️-the-fill-handle)** — Drag on the sheet instead of typing addresses, and drag the corner to fill a whole column — references shift correctly, and `$A$1` stays put -4. **[Conditional formatting](#-conditional-formatting)** — Values out of the ordinary colour themselves, so nobody has to read every row +1. **[Restrict what can be entered](#data-validation)** — A cell becomes a dropdown or accepts only numbers in a range. Anything outside the rule is **not saved** — refused, not stored and flagged later +2. **[See which cells a formula reads](#see-what-a-formula-is-about)** — Click the total and the cells it reads light up on the sheet — a range one column too wide is visible at a glance +3. **[Pick ranges by dragging](#the-fill-handle)** — Drag on the sheet instead of typing addresses, and drag the corner to fill a whole column — references shift correctly, and `$A$1` stays put +4. **[Conditional formatting](#conditional-formatting)** — Values out of the ordinary colour themselves, so nobody has to read every row > **Outcome:** Bad data is stopped at the moment it is typed, not found at month-end close @@ -193,9 +169,9 @@ the outcome under a double rule like a total. **Solved by** -1. **[Connect an API from your browser](#from-your-browser-restcsv--works-on-this-site)** — Enter the URL and header once, right on this site; an API behind a VPN works too if your machine can reach it · for [PostgreSQL / MySQL directly](#-straight-into-a-database-postgresql--mysql), run the app on your own server. Queries run read-only, and the database itself refuses writes -2. **[Press “Insert into sheet”](#-live-data-from-an-api--csv-prototype)** — Users pick a cell and choose the whole table or one summary figure. Values refresh on a schedule and feed formulas like any other cell -3. **[Pivots and charts that follow the data](#-pivot-summarise-a-range)** — A summary tied to its source refreshes with one press, and [charts](#-charts-from-the-sheet) move the moment the numbers do +1. **[Connect an API from your browser](#from-your-browser-restcsv--works-on-this-site)** — Enter the URL and header once, right on this site; an API behind a VPN works too if your machine can reach it · for [PostgreSQL / MySQL directly](#straight-into-a-database-postgresql--mysql), run the app on your own server. Queries run read-only, and the database itself refuses writes +2. **[Press “Insert into sheet”](#live-data-from-an-api--csv-prototype)** — Users pick a cell and choose the whole table or one summary figure. Values refresh on a schedule and feed formulas like any other cell +3. **[Pivots and charts that follow the data](#pivot-summarise-a-range)** — A summary tied to its source refreshes with one press, and [charts](#charts-from-the-sheet) move the moment the numbers do > **Outcome:** One monthly chore is gone, and the number in the file is the number right now @@ -208,10 +184,10 @@ the outcome under a double rule like a total. **Solved by** -1. **[Open .xlsx files as they were](#-it-looks-like-the-file-you-opened)** — Fills, borders, font sizes, row heights and merged cells all come across -2. **[Templates from locked files](#-templates-from-an-excel-file)** — A protected workbook is read as a template that knows which cells are for input and which must not be touched, dropdowns included -3. **[Exports you can keep working on](#-export)** — Formulas stay formulas, charts are real Excel charts, and [CSV exports](#-csv-in-and-out) neutralise formulas smuggled in with the data (CSV injection) -4. **[Thai PDFs that read correctly](#-export)** — The Thai font is embedded, and tone marks stacked over upper vowels land in the right place +1. **[Open .xlsx files as they were](#it-looks-like-the-file-you-opened)** — Fills, borders, font sizes, row heights and merged cells all come across +2. **[Templates from locked files](#templates-from-an-excel-file)** — A protected workbook is read as a template that knows which cells are for input and which must not be touched, dropdowns included +3. **[Exports you can keep working on](#export)** — Formulas stay formulas, charts are real Excel charts, and [CSV exports](#csv-in-and-out) neutralise formulas smuggled in with the data (CSV injection) +4. **[Thai PDFs that read correctly](#export)** — The Thai font is embedded, and tone marks stacked over upper vowels land in the right place > **Outcome:** No existing file left behind, and whatever you send out opens in Excel ready to keep working on @@ -224,10 +200,10 @@ the outcome under a double rule like a total. **Solved by** -1. **[Runs entirely in the browser](#-security--what-was-actually-tested)** — No account, no upload, the file never leaves the machine, and the page's security policy (CSP) closes the route for data to be sent anywhere else — an e2e flow fires at it for real · except asking the AI, which sends the question and column headers to Anthropic (with your key) or to this site's server (without one) -2. **[AI on your own key](#-bring-your-own-api-key-byok)** — Questions go from the browser straight to Anthropic on your key, never through this site's server — only the question, the selected range and the column headers, never the file -3. **[Usage counted without identifying anyone](#-a-usage-count-that-provably-cannot-identify-anyone)** — It can tell how many times the app was opened today, nothing more: no IP, no cookie, and Do Not Track is honoured · error reports (if switched on) strip Thai text and keys before they leave -4. **[Cloud on your own backend](#️-cloud-save-bring-your-own-backend)** — Want to save online or edit together? Connect your own Supabase project — and its row-level access rules are under test +1. **[Runs entirely in the browser](#security--what-was-actually-tested)** — No account, no upload, the file never leaves the machine, and the page's security policy (CSP) closes the route for data to be sent anywhere else — an e2e flow fires at it for real · except asking the AI, which sends the question and column headers to Anthropic (with your key) or to this site's server (without one) +2. **[AI on your own key](#bring-your-own-api-key-byok)** — Questions go from the browser straight to Anthropic on your key, never through this site's server — only the question, the selected range and the column headers, never the file +3. **[Usage counted without identifying anyone](#a-usage-count-that-provably-cannot-identify-anyone)** — It can tell how many times the app was opened today, nothing more: no IP, no cookie, and Do Not Track is honoured · error reports (if switched on) strip Thai text and keys before they leave +4. **[Cloud on your own backend](#cloud-save-bring-your-own-backend)** — Want to save online or edit together? Connect your own Supabase project — and its row-level access rules are under test > **Outcome:** Start using it without waiting for anyone's approval, and answer “where does the data go?” with code anyone can read, not with a policy page @@ -240,10 +216,10 @@ the outcome under a double rule like a total. **Solved by** -1. **[Autosave, and undo](#-autosave--undoredo)** — Every edit is kept in the browser as you make it, and Ctrl+Z steps back one change at a time -2. **[A save that fails says so](#-autosave--undoredo)** — If the browser refuses a save — storage full, for instance — the app shows an alert straight away and export still works, instead of saying nothing until the tab closes -3. **[Crashes still let the work out](#-the-app-can-break-and-you-still-get-your-file-out)** — If the app crashes, the screen that appears offers the work as a file before anything else, and the app [opens even with no connection](#-opens-with-the-network-off) -4. **[Edit together, with version history](#-editing-together)** — Several people edit one file live, your undo never erases theirs, and earlier versions can be restored (on your own Supabase project) +1. **[Autosave, and undo](#autosave--undoredo)** — Every edit is kept in the browser as you make it, and Ctrl+Z steps back one change at a time +2. **[A save that fails says so](#autosave--undoredo)** — If the browser refuses a save — storage full, for instance — the app shows an alert straight away and export still works, instead of saying nothing until the tab closes +3. **[Crashes still let the work out](#the-app-can-break-and-you-still-get-your-file-out)** — If the app crashes, the screen that appears offers the work as a file before anything else, and the app [opens even with no connection](#opens-with-the-network-off) +4. **[Edit together, with version history](#editing-together)** — Several people edit one file live, your undo never erases theirs, and earlier versions can be restored (on your own Supabase project) > **Outcome:** Nothing you've typed disappears quietly, and there is one copy of the file that everyone works on @@ -252,107 +228,111 @@ the outcome under a double rule like a total. --- -## 📋 Table of Contents - -- [Try it in 60 seconds](#️-try-it-in-60-seconds) -- [Screenshots](#-screenshots) -- [Problems it solves](#-problems-it-solves) -- [Why this project](#-why-this-project) -- [Getting started](#-getting-started) -- [Features](#-features) - - [Ask AI for a formula](#-ask-ai-for-a-formula) - - [Bring your own API key (BYOK)](#-bring-your-own-api-key-byok) - - [Live data from an API / CSV (prototype)](#-live-data-from-an-api--csv-prototype) - - [Straight into a database (PostgreSQL / MySQL)](#-straight-into-a-database-postgresql--mysql) - - [Connect your own data: the in-app guide](#-connect-your-own-data-the-in-app-guide) - - [Spreadsheet grid](#-spreadsheet-grid) - - [Autosave + Undo/Redo](#-autosave--undoredo) - - [The app can break and you still get your file out](#-the-app-can-break-and-you-still-get-your-file-out) - - [Copy / Cut / Paste](#️-copy--cut--paste) - - [Cell formatting](#-cell-formatting) - - [Fill colour](#-fill-colour) - - [Phone numbers and codes keep their zeros](#-phone-numbers-and-codes-keep-their-zeros) - - [Dates and times](#-dates-and-times) - - [Buddhist-Era dates](#-buddhist-era-dates) - - [Charts from the sheet](#-charts-from-the-sheet) - - [Conditional formatting](#-conditional-formatting) - - [Cell comments](#-cell-comments) - - [Data validation](#-data-validation) - - [Named ranges](#-named-ranges) - - [Cloud save (bring your own backend)](#️-cloud-save-bring-your-own-backend) - - [Editing together](#-editing-together) - - [The Excel keyboard](#️-the-excel-keyboard) - - [Formulas that answer with a whole table (array formulas)](#-formulas-that-answer-with-a-whole-table-array-formulas) - - [Formulas across sheets](#-formulas-across-sheets) - - [See what a formula is about](#-see-what-a-formula-is-about) - - [The fill handle](#️-the-fill-handle) - - [Find and replace](#-find-and-replace) - - [Opens with the network off](#-opens-with-the-network-off) - - [Works on a phone](#-works-on-a-phone) - - [Copy, paste and fill down with a finger](#-copy-paste-and-fill-down-with-a-finger) - - [Selecting a range with a finger without losing the data](#-selecting-a-range-with-a-finger-without-losing-the-data) - - [Tapping cells into a formula on a phone](#-tapping-cells-into-a-formula-on-a-phone) - - [Tablets and folding phones](#-tablets-and-folding-phones) - - [One message at a time, and short screens](#-one-message-at-a-time-and-short-screens) - - [Insert/delete rows & columns](#-insertdelete-rows--columns) - - [Right-click on a cell](#️-right-click-on-a-cell) - - [Drag a column wider or narrower](#-drag-a-column-wider-or-narrower) - - [Merging cells](#-merging-cells) - - [Sort and filter](#-sort-and-filter) - - [Pivot (summarise a range)](#-pivot-summarise-a-range) - - [Multiple sheets in one file](#-multiple-sheets-in-one-file) - - [Import an existing Excel file](#-import-an-existing-excel-file) - - [Told up front what a file can't keep](#-told-up-front-what-a-file-cant-keep) - - [Opening a file without losing your work](#-opening-a-file-without-losing-your-work) - - [A blank start, and New file](#-a-blank-start-and-new-file) - - [Drag-and-drop formulas](#-drag-and-drop-formulas) - - [Export](#-export) - - [CSV in and out](#-csv-in-and-out) - - [It looks like the file you opened](#-it-looks-like-the-file-you-opened) - - [Templates from an Excel file](#-templates-from-an-excel-file) - - [A usage count that provably cannot identify anyone](#-a-usage-count-that-provably-cannot-identify-anyone) - - [A landing page that explains the app](#-a-landing-page-that-explains-the-app) - - [Questions people ask, on the landing page](#-questions-people-ask-on-the-landing-page) - - [One page per formula](#-one-page-per-formula) - - [Bilingual (Thai / English)](#-bilingual-thai--english) -- [Tech stack](#-tech-stack) -- [Architecture](#-architecture) -- [Project structure](#-project-structure) -- [Formula engine](#-formula-engine) -- [Bilingual UI (i18n)](#-bilingual-ui-i18n) -- [Security — what was actually tested](#-security--what-was-actually-tested) -- [Lighthouse](#-lighthouse) -- [Testing](#-testing) -- [What's next](#-whats-next) +## Table of Contents + +- [Overview](#overview) +- [Key capabilities](#key-capabilities) +- [Data handling](#data-handling) +- [Try it in 60 seconds](#try-it-in-60-seconds) +- [Screenshots](#screenshots) +- [Problems it solves](#problems-it-solves) +- [Project goals](#project-goals) +- [Getting started](#getting-started) +- [Features](#features) + - [Ask AI for a formula](#ask-ai-for-a-formula) + - [Bring your own API key (BYOK)](#bring-your-own-api-key-byok) + - [Live data from an API / CSV (prototype)](#live-data-from-an-api--csv-prototype) + - [Straight into a database (PostgreSQL / MySQL)](#straight-into-a-database-postgresql--mysql) + - [Connect your own data: the in-app guide](#connect-your-own-data-the-in-app-guide) + - [Spreadsheet grid](#spreadsheet-grid) + - [Autosave + Undo/Redo](#autosave--undoredo) + - [The app can break and you still get your file out](#the-app-can-break-and-you-still-get-your-file-out) + - [Copy / Cut / Paste](#copy--cut--paste) + - [Cell formatting](#cell-formatting) + - [Fill colour](#fill-colour) + - [Phone numbers and codes keep their zeros](#phone-numbers-and-codes-keep-their-zeros) + - [Dates and times](#dates-and-times) + - [Buddhist-Era dates](#buddhist-era-dates) + - [Charts from the sheet](#charts-from-the-sheet) + - [Conditional formatting](#conditional-formatting) + - [Cell comments](#cell-comments) + - [Data validation](#data-validation) + - [Named ranges](#named-ranges) + - [Cloud save (bring your own backend)](#cloud-save-bring-your-own-backend) + - [Editing together](#editing-together) + - [The Excel keyboard](#the-excel-keyboard) + - [Formulas that answer with a whole table (array formulas)](#formulas-that-answer-with-a-whole-table-array-formulas) + - [Formulas across sheets](#formulas-across-sheets) + - [See what a formula is about](#see-what-a-formula-is-about) + - [The fill handle](#the-fill-handle) + - [Find and replace](#find-and-replace) + - [Opens with the network off](#opens-with-the-network-off) + - [Works on a phone](#works-on-a-phone) + - [Copy, paste and fill down with a finger](#copy-paste-and-fill-down-with-a-finger) + - [Selecting a range with a finger without losing the data](#selecting-a-range-with-a-finger-without-losing-the-data) + - [Tapping cells into a formula on a phone](#tapping-cells-into-a-formula-on-a-phone) + - [Tablets and folding phones](#tablets-and-folding-phones) + - [One message at a time, and short screens](#one-message-at-a-time-and-short-screens) + - [Insert/delete rows & columns](#insertdelete-rows--columns) + - [Right-click on a cell](#right-click-on-a-cell) + - [Drag a column wider or narrower](#drag-a-column-wider-or-narrower) + - [Merging cells](#merging-cells) + - [Sort and filter](#sort-and-filter) + - [Pivot (summarise a range)](#pivot-summarise-a-range) + - [Multiple sheets in one file](#multiple-sheets-in-one-file) + - [Import an existing Excel file](#import-an-existing-excel-file) + - [Told up front what a file can't keep](#told-up-front-what-a-file-cant-keep) + - [Opening a file without losing your work](#opening-a-file-without-losing-your-work) + - [A blank start, and New file](#a-blank-start-and-new-file) + - [Drag-and-drop formulas](#drag-and-drop-formulas) + - [Export](#export) + - [CSV in and out](#csv-in-and-out) + - [It looks like the file you opened](#it-looks-like-the-file-you-opened) + - [Templates from an Excel file](#templates-from-an-excel-file) + - [A usage count that provably cannot identify anyone](#a-usage-count-that-provably-cannot-identify-anyone) + - [A landing page that explains the app](#a-landing-page-that-explains-the-app) + - [Questions people ask, on the landing page](#questions-people-ask-on-the-landing-page) + - [One page per formula](#one-page-per-formula) + - [Bilingual (Thai / English)](#bilingual-thai--english) +- [Tech stack](#tech-stack) +- [Architecture](#architecture) +- [Project structure](#project-structure) +- [Formula engine](#formula-engine) +- [Bilingual UI (i18n)](#bilingual-ui-i18n) +- [Security — what was actually tested](#security--what-was-actually-tested) +- [Lighthouse](#lighthouse) +- [Testing](#testing) +- [What's next](#whats-next) --- -## 🎯 Why this project - -The starting point was a real pain point using Excel: **hard to fill in on desktop/online, an unfriendly UI, -formulas nobody remembers, and getting lost scrolling a big sheet.** This project sets out to fix that directly -rather than build a generic "calculator app," which means dealing with more subtlety than it first looks: - -- Users should be able to type formulas themselves **or** drag one in without knowing any syntax — both paths - have to work equally well. -- Formulas must shift references correctly both when copied/filled down a column (relative) **and** when a row - or column is inserted/deleted (structural) — two genuinely different algorithms. Get either wrong and the - sheet silently computes the wrong number. -- Formulas can reference each other in a circle; that has to be detected, not hang the app. -- Every edit (typing a value, inserting a formula, formatting) needs undo/redo, but moving the selection or - switching sheet tabs must **not** count as history — otherwise one Ctrl+Z would just undo where the cursor - was, not the actual content change. -- Importing/exporting Excel files has to preserve **the original formulas and formatting** (bold/color/alignment), - not just the computed numbers. -- It should work for both Thai and English speakers without needing locale-based routing (`/en/...`), since the - app is entirely client-rendered. - -The project's focus is therefore **correctness of the calculation logic + an architecture that's maintainable**, -rather than a long feature list with shallow depth — see [Formula engine](#-formula-engine) for the deep dive. +## Project goals + +The project began with practical problems in everyday Excel use: **data entry is awkward on desktop and on the +web, the interface is unfriendly, formulas are hard to remember, and it is easy to lose track of the row or column +while scrolling a large sheet.** The aim is to address those problems directly rather than to build a generic +calculator, which raises several technical requirements: + +- Users must be able to type formulas themselves **and** drag one in without knowing any syntax; both paths must + work equally well. +- Formulas must adjust their references correctly both when copied or filled down a column (relative) **and** + when a row or column is inserted or deleted (structural). These are two different algorithms, and an error in + either produces wrong results without any visible sign. +- Circular references must be detected without the application hanging. +- Every edit (entering a value, inserting a formula, formatting) must support undo and redo, while moving the + selection or switching sheets must **not** be recorded as history; otherwise Ctrl+Z would undo the cursor + position rather than the actual change. +- Importing and exporting Excel files must preserve **the original formulas and formatting** (bold, colour, + alignment), not only the computed values. +- The application must serve both Thai and English speakers without locale-based routing (`/en/...`), since it + is rendered entirely on the client. + +The project therefore prioritises **correct calculation logic and a maintainable architecture** over the number +of features. See [Formula engine](#formula-engine) for the technical detail. --- -## 🚀 Getting started +## Getting started > About 2 minutes · no database or separate backend needed — everything runs in one Next.js app @@ -381,10 +361,10 @@ Open **http://localhost:3000** and you land on a page explaining what the app do | Route | What it is | |---|---| | `/` | The landing page — features with screenshots from the running app, bilingual like the app itself | -| `/app` | The app itself, opening on a sample sheet with nothing to configure | +| `/app` | The application, which opens on a blank sheet with a **Try it with sample data** button on the green bar; nothing to configure | -(The sample data is a coffee/bread/milk receipt with real total formulas. Click **ExcelToGo** in the -app's top-left corner to get back to the landing page.) +(The sample data is a list of coffee, bread and milk sales with working total formulas. Click **ExcelToGo** +in the application's top-left corner to return to the landing page.) Other available commands: @@ -469,7 +449,7 @@ This is a standard Next.js app, so it deploys to any platform that supports Next > control.** > > The one billing control is **not setting the key on a public deployment.** The local keyword matcher answers -> instead, for free, and visitors can still bring [their own key](#-bring-your-own-api-key-byok), which goes from +> instead, for free, and visitors can still bring [their own key](#bring-your-own-api-key-byok), which goes from > their browser straight to Anthropic and never touches this route. That is how this project's public site is meant to run · > `NEXT_PUBLIC_DEMO_MODE=1` used to make the route skip Anthropic even with a key set — **it no longer does** > (#109), and [`route.test.ts`](src/app/api/ai/formula/route.test.ts) pins that the old switch no longer hides a @@ -489,7 +469,7 @@ This is a standard Next.js app, so it deploys to any platform that supports Next > variable used to be what made a leftover key or token harmless; now a key takes effect the moment this version > deploys, and a token the moment the variable is removed. -### 🔧 Troubleshooting +### Troubleshooting
Click to expand @@ -508,9 +488,9 @@ This is a standard Next.js app, so it deploys to any platform that supports Next --- -## ✨ Features +## Features -### 🤖 Ask AI for a formula +### Ask AI for a formula Type what you want as a plain sentence, in Thai or English — e.g. _"I want to total all sales in this column."_ The app sends your question plus the currently selected range to the AI and gets back a suggested formula with @@ -614,7 +594,7 @@ a condition is declined, which errs on the safe side. Repeatable with `npm run check:ai` (needs your own key; not part of `npm run verify`, because it costs money). -### 🔑 Bring your own API key (BYOK) +### Bring your own API key (BYOK) The assistant answers one of three ways, and the landing page says plainly which ones the public site uses (the keyword matcher, or your own key): @@ -645,7 +625,7 @@ second is never called (`hit our own /api/ai/formula: false`); the first arrives > The question, the selected range and the column headers go to Anthropic only when the button is > pressed — **the file itself is never sent**, and if nobody presses it, nothing leaves the machine. -### 🔌 Live data from an API / CSV (prototype) +### Live data from an API / CSV (prototype)

Picking a live source, pressing it into the sheet, and the table changing on its own every five seconds

@@ -724,7 +704,7 @@ origin on this path is the browser itself (CORS, mixed content, private-network which allows only the origins that user has added. **A new API costs one page reload.** The CSP's `connect-src` is narrow for everyone, because it is what stops -an injected script from sending [the visitor's own API key](#-bring-your-own-api-key-byok) anywhere else; it +an injected script from sending [the visitor's own API key](#bring-your-own-api-key-byok) anywhere else; it cannot be opened for everybody for the sake of this feature. So on save the app writes **only the origin** of the URL (scheme + host + port, no path, no query) into a cookie, `etg-api-origins`, and `src/proxy.ts` adds that origin to *that user's* policy only. A page's policy is fixed when it loads, so a new origin needs one reload: @@ -735,7 +715,7 @@ reloads for you and picks up where it was — running the test, or opening the p - Someone who never adds a source gets the same CSP, **character for character** — e2e checks the real header before and after. - Deleting the last source for an origin takes that origin out of the cookie. -- The security trade-off is written down plainly under [Security](#-security--what-was-actually-tested) and in +- The security trade-off is written down plainly under [Security](#security--what-was-actually-tested) and in [`SECURITY.md`](SECURITY.md). #### What to ask IT for @@ -774,9 +754,9 @@ error — naming the header IT has to send, with the site's own origin, and aski A browser cannot connect to PostgreSQL or MySQL directly — a web page cannot open a TCP connection to a database. The simplest route is the company's or the ERP's API, the way above. If you want the **server** to do the fetching -— a [direct database connection](#-straight-into-a-database-postgresql--mysql), a URL and credential hidden from the +— a [direct database connection](#straight-into-a-database-postgresql--mysql), a URL and credential hidden from the people using the sheet, or somewhere the server can reach and users' machines cannot — run the app on your own -server (the [in-app guide](#-connect-your-own-data-the-in-app-guide) has a four-step starter kit). +server (the [in-app guide](#connect-your-own-data-the-in-app-guide) has a four-step starter kit). **This part appears in the UI only when the deployment sets `SOURCES_ADMIN_TOKEN`.** The root layout checks on every request and marks the page (a yes/no, never the token). Without it there is no token box and no "this @@ -866,7 +846,7 @@ used to do, with a SUM then reading the envelope's `total` as data. If the envel the app does not guess which one the table is: it says it cannot read a table, lists the keys, and leaves the JSON path to you. Both browser and server sources behave this way, since they share the converter. -**CSV sources and codes with leading zeros** — each CSV field is read by [the grid's own rule](#-phone-numbers-and-codes-keep-their-zeros), +**CSV sources and codes with leading zeros** — each CSV field is read by [the grid's own rule](#phone-numbers-and-codes-keep-their-zeros), so `007`, `0812345678` and all-digit values of 12 digits or more arrive as text, exactly as sent, while `12.5` and `-3` are still numbers. (Every field used to go through `Number()`, so a branch code `007` reached the sheet as `7`.) JSON sources are unchanged: their types come from the JSON, and a string `"123"` in JSON still lands as a @@ -966,7 +946,7 @@ Behind the scenes: - **No company cookie / single sign-on auth** (`credentials: "include"`) **and no OAuth yet** — only a header you type in. - **No "remember the token on this device"** — close the tab and the header value has to be entered again. That - is on purpose (see the table above); an opt-in version is on the [roadmap](#-whats-next). + is on purpose (see the table above); an opt-in version is on the [roadmap](#whats-next). - **The API has to allow CORS for the site you are on** — the app cannot do that for it. If the API's owner will not, the remaining route is a server-side source. - **An `http://` API other than localhost does not work from the https site** — the browser blocks it (mixed @@ -978,10 +958,10 @@ Behind the scenes: - **Server-side sources do not work on Vercel** — they are stored in `data/sources.json`, and Vercel's disk is read-only. Run on a VPS, Docker with a volume, or a machine in the office. (Connecting from the browser works on Vercel as normal, because nothing is stored on the server.) -- **There is no follow-along sample on the site yet** — the three sample sources that used to be put there are - gone; a step-by-step sample is on the [roadmap](#-whats-next). +- **The three former server-side sample sources have been removed** and replaced by + [three sample APIs](#try-a-sample-api) connected through the same form as the user's own API. -### 🗄 Straight into a database (PostgreSQL / MySQL) +### Straight into a database (PostgreSQL / MySQL) A [server-side source](#databases--server-side-sources-self-host) — it needs the app running on your own server with `SOURCES_ADMIN_TOKEN` set, because a browser cannot reach a database directly. Identical to a server-side @@ -1034,7 +1014,7 @@ table picker yet (the SQL is typed), and **no test connects to a real database** touches a driver is kept as thin as it can be, and every judgement call lives in pure modules that are tested without one. -### 📘 Connect your own data: the in-app guide +### Connect your own data: the in-app guide **`/guide`** is where the app sends someone who wants their own API or database: from the phone menu, from the link at the foot of the live-data panel, and from the landing page's footer. @@ -1062,7 +1042,7 @@ your own. The page is what this README and `SECURITY.md` already say, in fewer w changes, it has to follow — and no gate compares them yet. -### 📐 Spreadsheet grid +### Spreadsheet grid - Click to select a cell, **double-click**/**F2** to edit, or just start typing to overwrite it directly. - An always-visible **formula bar** shows the selected cell's address and raw content, just like Excel — edit @@ -1074,7 +1054,7 @@ changes, it has to follow — and no gate compares them yet. - Row/column headers are sticky and highlighted for the current selection — fixes the "scrolled and now I'm lost which row I'm on" problem. -### 💾 Autosave + Undo/Redo +### Autosave + Undo/Redo - Every edit (typing a value, inserting a formula, formatting, importing a file) autosaves to `localStorage` immediately — closing the tab or refreshing keeps your data (tied to that browser/device only, no cross-device @@ -1116,7 +1096,7 @@ changes, it has to follow — and no gate compares them yet.

Phone: one line saying the work is saved in this browser only, and the save status's popover on the top bar with the full text and Export Excel

-### 🩹 The app can break and you still get your file out +### The app can break and you still get your file out When a render throws, what you used to get was Next's bare crash page: no explanation, no way back, and — the part that actually matters in a spreadsheet — no sign of whether the work was gone. **It was @@ -1181,7 +1161,7 @@ The report endpoint's origin joins `connect-src` automatically. A collector that not in the policy would be blocked silently, which is worse than having none — the operator would believe they had one. -### ✂️ Copy / Cut / Paste +### Copy / Cut / Paste - **Ctrl+C / Ctrl+X / Ctrl+V** with a dashed highlight showing status (blue = copied, orange = cut). - Pasting a copied formula adjusts relative references automatically, just like Excel. @@ -1199,10 +1179,10 @@ believe they had one. as in Excel (checked in real Excel). **Not supported yet:** a range reaching into the landing area stays as it was, and the source cells keep their formatting rather than it moving with them. -### 🎨 Cell formatting +### Cell formatting Bold, **italic, underline**, text alignment (left/center/right), text color, number format (general / 2 decimal places / percent / -currency ฿ / [text](#-phone-numbers-and-codes-keep-their-zeros)) — travels with the cell on copy/paste and survives Excel export too. +currency ฿ / [text](#phone-numbers-and-codes-keep-their-zeros)) — travels with the cell on copy/paste and survives Excel export too.

@@ -1219,7 +1199,7 @@ exported as it came. Picking a format from the menu replaces it with the app's. 50.00%, not 5000%, in the browser and in cloud workbooks. It becomes the `0.00"%"` code the app used to write, once, on load; choosing Percent again gives Excel's. -Typing `50%` into a cell gives the number 0.5 now (#52; see [Phone numbers and codes keep their zeros](#-phone-numbers-and-codes-keep-their-zeros)). +Typing `50%` into a cell gives the number 0.5 now (#52; see [Phone numbers and codes keep their zeros](#phone-numbers-and-codes-keep-their-zeros)). **Limits:** A file's fractions (`# ?/?`) and scientific codes (`0.00E+00`) show the plain number. Codes cannot be written in the app yet, only brought in with a file. A cloud @@ -1238,7 +1218,7 @@ three stacked bars ate 150px before a single grid row appeared; folded, that's 1 without opening a cell. They used to do nothing at all in the grid, though they are among the first keys someone arriving from Excel tries. They are listed in the shortcuts dialog under "Text style". -### 🪣 Fill colour +### Fill colour The **paint bucket** in the formatting row opens seven swatches and "no fill", applied to the whole selection; it travels with copy/paste and the `.xlsx` export and is one undo step. The bar under the bucket shows the @@ -1252,11 +1232,11 @@ The swatches are the lightest row of Excel's own palette, so a header stays read at home there; a colour wheel (``) on a phone is a small, fiddly target that invites a dark fill under dark text. -Fills and text colours **now reach the PDF export too** (see [Export](#-export)). +Fills and text colours **now reach the PDF export too** (see [Export](#export)). **Not supported yet:** a colour outside these seven (fills from an imported file still show and export in full). -### 🔢 Phone numbers and codes keep their zeros +### Phone numbers and codes keep their zeros Type a phone number `0812345678`, a Thai ID card `1234567890123` or a product code `00123` and **you get what you typed** — on the grid, in `.xlsx`, CSV and PDF, when sorting, filtering, in a pivot and in a chart. The leading @@ -1301,11 +1281,11 @@ always stored as typed, so the zeros come back the moment they are opened. - **What was already lost stays lost** — an `.xlsx` exported or imported before the fix holds `812345678` as a number, with no zero to give back. (Live data from a CSV source follows the same rule since #36.) - **`=SUM(A1:A3)` over these codes is 0 now, as in Excel (#166, #38)** — text read from cells is not added, - so an old sheet with codes inside a summed range now totals what Excel gives (see [Formula engine](#-formula-engine)). + so an old sheet with codes inside a summed range now totals what Excel gives (see [Formula engine](#formula-engine)). - "Text" is a cell format, so like every format it does not sync live while co-editing; an apostrophe is part of the cell, so it does. -### 📅 Dates and times +### Dates and times Type `2024-01-15` and you get **a date**, not text that looks like one. Its value is the serial Excel uses (`2024-01-15` is 45306, noon is .5), so `=A2-A1` is a count of days, `=A1+30` is the date a month @@ -1323,7 +1303,7 @@ on, and an exported file opens in Excel with **date cells** rather than 45306 or - **ISO forms only** (`yyyy-mm-dd`). `03/04/2024` is March in one country and April in another, and a wrong guess moves a date a month without a word, so every other layout stays the text it is — - except the Thai forms that cannot be misread; see [Buddhist-Era dates](#-buddhist-era-dates). + except the Thai forms that cannot be misread; see [Buddhist-Era dates](#buddhist-era-dates). - **Excel's 1900 system**, including the 29 February 1900 that never happened (Excel inherited it from Lotus 1-2-3), so a date typed here is the same number in Excel on every day there is. - **Opening an `.xlsx`** keeps the time; a time-only cell is `09:45` (it used to become `1899-12-30`), @@ -1356,7 +1336,7 @@ to pad each sheet with empty rows to the grid's size, so a 5-row sheet came back you pick "Date" (Excel infers it from the formula; this does not yet). Only formulas starting with `TODAY`, `NOW` or `DATE` are formatted automatically. - **Ambiguous date layouts stay text**, whether typed, pasted, in a CSV or from live data: `15/01/2024` - cannot be subtracted until the [Convert to dates](#-buddhist-era-dates) command reads it (#82). + cannot be subtracted until the [Convert to dates](#buddhist-era-dates) command reads it (#82). - **Gregorian `d/m/yyyy` text is `#VALUE!` in the date functions too (#169).** `=DAY(A1)` over the text `1/9/2026` is `#VALUE!`, as `=A1+1` already was, in `DAY`, `MONTH`, `YEAR` and `DATEDIF` alike. Those four used to read it themselves, US-style (9 January) and shifted by the machine's time zone, so they gave 8 in @@ -1367,7 +1347,7 @@ to pad each sheet with empty rows to the grid's size, so a 5-row sheet came back - A date format is a cell format, so it does not sync live between collaborators yet (the existing limit on every format). The date itself lives in the cell, so that syncs. -### 📆 Buddhist-Era dates +### Buddhist-Era dates Thai files very often hold dates as `15/01/2569`, `15 ม.ค. 2569` or `15/01/69` (feedback from real users, #82). Read literally, every one of them is 543 years off, and `DATEDIF`, length of service, days overdue and @@ -1419,7 +1399,7 @@ are left alone, and so are the rows a filter hides, as every command on a filter still read it as a date, and Convert to dates makes it one. - Thai digits (`๑๕/๐๑/๒๕๖๙`) and times written `13.45 น.` are not read yet. -### 📊 Charts from the sheet +### Charts from the sheet Select a range, hit **Charts** in the format bar, and pick **bar, line or pie**. The chart lands on the grid just under the range it reads: **drag the bar at its top to move it, the bottom-right @@ -1492,7 +1472,7 @@ was right. draws one series at a time. The chart parts carry no colour or theme styling, so whichever app opens the file applies its own defaults. -### ☁️ Cloud save (bring your own backend) +### Cloud save (bring your own backend) **Off by default, deliberately.** ExcelToGo is an open-source project, **not a hosted service**. Running one database for everyone who uses the app would make the maintainer a data controller with @@ -1563,7 +1543,7 @@ Renaming a workbook does not make a version, or a rename would push a real one o **Not supported:** automatic sync (you press save). -### 👥 Editing together +### Editing together Save a workbook to your Supabase project, share it with someone, press **"Join the live session"** in the cloud panel, and everyone who can open that workbook sees the others type, with a coloured @@ -1615,7 +1595,7 @@ Everything arriving on the channel **is still checked before it is used**, becau says who may speak, not that what they said is well formed. A `row` of `-1` reaches an array index, and a `raw` that is not a string reaches the formula engine. -### 💬 Cell comments +### Cell comments Select a cell, hit **Comment** in the format bar, and write a note. A commented cell gets an **amber corner**; hover to read it. @@ -1643,7 +1623,7 @@ Google Sheets; and **a note on an empty cell is lost when the file is read back itself is correct and Excel shows the note, but ExcelJS attaches notes only to cells present in its sheet model, and a cell with no value isn't. The loss is in the reader, not the writer. -### 🛡 Data validation +### Data validation Select a range, hit **Limit** in the format bar, and say what those cells will accept: one of a list, a number in a range, or a length cap. A cell carrying a rule gets a **thin emerald ring**, @@ -1695,7 +1675,7 @@ contracted. A hidden or protected sheet comes back out as an ordinary one. A tem `list` rules — `whole`/`decimal`/`textLength`/`date` on a protected sheet are dropped. All of it has to be fixed before the [PaynEat ERP import template](docs/payneat-erp.en.md). -### 🏷 Named ranges +### Named ranges `=SUMIF(Sales,">1000")` against `=SUMIF(B2:B500,">1000")`: the second makes every reader go and look at what is in column B, and gives the ones who guess wrong no way to find out. The formula bar is @@ -1739,7 +1719,7 @@ sheet-level while making one is not there yet. They go **into and out of `.xlsx` as real defined names, scope and all**: a workbook-level name stays workbook-level, one with a `localSheetId` stays on the sheet the file names (not the one it points at), and an export opened again comes back with the same scopes. **A name that cannot come in is named when -the file opens**, in the same report as everything else the file can't keep ([#83](#-told-up-front-what-a-file-cant-keep)) — a formula (`=OFFSET(...)`), a whole column (`$B:$B`), several areas, or a sheet the file +the file opens**, in the same report as everything else the file can't keep ([#83](#told-up-front-what-a-file-cant-keep)) — a formula (`=OFFSET(...)`), a whole column (`$B:$B`), several areas, or a sheet the file does not have — rather than turning into `#NAME?` with nothing to say why. **Stated limits:** formula and whole-column names still cannot come in (they are named instead). When a @@ -1750,7 +1730,7 @@ it. And **deleting a name leaves the formulas holding it**, reading `#NAME?` rather than being rewritten back to addresses: quietly rewriting work nobody asked to have rewritten is worse, and `#NAME?` is both findable and undoable. -### 🌡 Conditional formatting +### Conditional formatting Colours that follow the numbers, instead of being painted on once and going stale the moment a value changes. Select a range, hit **Conditional formatting** in the format bar, and add one of @@ -1780,7 +1760,7 @@ reach the PDF export** (neither do fills or bold, which it doesn't carry either) several rules hit one cell, **the lower rule wins** — the opposite of Excel's top-priority-wins order. That's chosen so a rule you just added visibly does something instead of silently nothing. -### ⌨️ The Excel keyboard +### The Excel keyboard Somebody who opens a spreadsheet moves their hands before they read anything — `Ctrl+Down` comes before the first word of the page. A grid that answers by moving one row has told them it is a @@ -1800,7 +1780,7 @@ mock-up. | `Ctrl+Enter` | Put the cell the cursor is on into everything selected, in one undo step — references shift as they would in a drag-fill, because a formula that kept pointing at the anchor's row would fill a column with the same wrong number | | `Tab` / `Shift+Tab` · `Enter` / `Shift+Enter` | Right/left · down/up — **Tab along a row and Enter comes back under the column you started in**, as in Excel, so a table is typed row by row without walking back to the left · **Tab at the last column adds a column** rather than staying put and typing over what was just entered (on a template that cannot grow, it behaves as Enter) · **Enter or ↓ on the last row adds a row (#170)**, so the sheet grows as you type down past its end, and one undo takes back what was typed there together with the row (on a template that cannot grow, the cursor stays) · on a phone, an undo while the editor is still open on a row it takes away drops the text not yet entered there, and says so (#191) | | A toolbar button | **Pressing it leaves the cursor on the grid** — arrows and Delete work straight away, with no click back into a cell (the buttons are still reachable with Tab) | -| `Shift+F10` / the Menu key | The [cell menu](#️-right-click-on-a-cell) for anyone who cannot right-click, opened at the selected cell | +| `Shift+F10` / the Menu key | The [cell menu](#right-click-on-a-cell) for anyone who cannot right-click, opened at the selected cell | | `F2` · `Delete` · `Escape` | Edit in place · clear the selection · cancel | The view **follows the cursor in both axes**, which is not a nicety here: `Ctrl+Down` can move five @@ -1832,7 +1812,7 @@ fails, a key listed and handled nowhere fails, and if the extraction itself ever third test catches that too — otherwise both of the others would pass by finding nothing. All three were confirmed by breaking the code on purpose and watching them go red. -Two more things the gates could not have told me, both found by looking: +Two further problems that no gate could have reported were found by inspection: - The button went in the toolbar first. At 1363px that row fitted its thirteen buttons with nothing to spare, and one more pushed 42px past the edge, clipping the language toggle on every laptop @@ -1841,9 +1821,9 @@ Two more things the gates could not have told me, both found by looking: this dialog does not exist until you press something. Run against it open, axe found two serious violations inside it — headings at 2.62:1, and a scrolling list no keyboard could reach. Both are fixed, and **the gate now opens the dialog and checks it too**, so the next one gets caught by CI - rather than by me remembering to look. + rather than by someone remembering to look. -### 🧮 Formulas that answer with a whole table (array formulas) +### Formulas that answer with a whole table (array formulas) Most formulas answer with one value. Some answer with a *shape* — `=UNIQUE(B2:B10)` has as many answers as there are distinct categories, which nobody knows while typing it. So those answers @@ -1895,7 +1875,7 @@ showing up once. > In an exported `.xlsx` the formula is written at the anchor, so a version of Excel that knows > dynamic arrays spills it again and an older one shows a single value. -### 🔗 Formulas across sheets +### Formulas across sheets `=Sheet2!A1` used to be `#SYNTAX!`. The app has had tabs, and a pivot that reads its source sheet, since early on — but every sheet was an island as far as a formula was concerned, and a real `.xlsx` @@ -1941,7 +1921,7 @@ a test fails if anything outside the engine calls `computeSheet` bare, and the c together with what it read from other sheets, so it can be checked every time — not only while it is still among the last four computed. -### 🔍 See what a formula is about +### See what a formula is about Select a cell holding a formula and the cells it reads are outlined on the grid, with the ranges written out beside the formula bar. @@ -1971,7 +1951,7 @@ nine thousand. What was missing was showing the person the same answer. - **Not shown while the editor is open**, where the text changes on every keystroke and the range picker is already doing this job better. -### 🖱️ The fill handle +### The fill handle The first thing anyone does to a spreadsheet is drag the corner. This app had the corner grip — touch uses it to pull a selection out — and nothing behind it on a mouse. @@ -2016,7 +1996,7 @@ The grip is split by pointer type rather than shown to everyone: a finger cannot other way, so touch keeps it for selecting. And the fill commits on release rather than filling live, which matters for undo as much as for nerves — one drag is one step back, not one per cell. -### 🔎 Find and replace +### Find and replace `Ctrl+F` is reflex, and it did nothing here — which became *more* conspicuous, not less, the moment a shortcut sheet went in advertising "the same keys as Excel". @@ -2040,7 +2020,7 @@ It deliberately preempts the browser's own find bar, which searches the DOM — forty rows the grid has decided to render, so on a five-thousand-row sheet it would report "not found" for text that is plainly there. -### 📴 Opens with the network off +### Opens with the network off The pitch has always been that the spreadsheet lives in your browser and is never uploaded. That was true, and the app still could not open on a train — the document was local and the *program* @@ -2073,7 +2053,7 @@ No screenshot: the install prompt is the browser's own chrome and looks differen them. The e2e gate covers it instead — it registers the worker, switches the network off, reloads, and checks the grid is there rather than an error page. -### 📱 Works on a phone +### Works on a phone Opening this on a phone used to show **not one cell of the spreadsheet** — the 320px side panel squeezed the grid down to its row numbers, and the toolbar wrapped into three rows that ate 380px @@ -2087,7 +2067,7 @@ of an 844px screen before the grid began. All three are fixed: | **Editing a cell** | **Tap to select, tap again to edit** — it previously needed a double-click, which a phone cannot do, so nothing could be typed at all · The second tap puts **the caret at the end of the text** rather than selecting it all (the first letter typed used to wipe the cell) · **Enter moves into the next cell ready to type, and the keyboard stays up** — it used to fold every time, so ten values meant ten taps to reopen it · When that cell already holds a value, **the value is selected and typing replaces it**, as on a desktop and in Excel (#202: it used to go on the end, so 599 became 599650); a second tap still edits in place · **One tap and the cell says "Tap again to type"**, and a keyboard left up by the cell before goes away (#171, below) | | **Tap targets** | 44×44 everywhere, in both bars (up from 28px — and five format-bar buttons were still only 32px wide until a later measurement caught them) | | **Selecting a range** | A **grip on the selection's bottom-right corner**, dragged to pull the range out — its touch target is 44×44 while the dot you see stays small | -| **Copy / paste / fill down** | **A row of commands above the sheet tabs** — see [Copy, paste and fill down with a finger](#-copy-paste-and-fill-down-with-a-finger) | +| **Copy / paste / fill down** | **A row of commands above the sheet tabs** — see [Copy, paste and fill down with a finger](#copy-paste-and-fill-down-with-a-finger) | | **Hover-revealed buttons** | Shown permanently where nothing hovers — otherwise the column filter and the delete-sheet button are invisible | ![On a phone](public/screenshots/en/19-mobile.png) @@ -2111,7 +2091,7 @@ while a keyboard is up, and every sheet closes itself once a tool is pressed, so The buttons in the cell tools sheet are **the same elements as the desktop row, not copies**: the box around them is `min-[1366px]:contents`, so from 1366px up it dissolves back into the row, from 1024 to 1365px it is a -panel dropped under the bar ([tablets](#-tablets-and-folding-phones)), and on a phone it becomes the sheet. +panel dropped under the bar ([tablets](#tablets-and-folding-phones)), and on a phone it becomes the sheet. A screen reader and the gates see one set of names. `check:a11y` opens both sheets and scans them at 390px, and one e2e flow asks plainly: "at 390px, is the way to live data on screen and named?" @@ -2169,7 +2149,7 @@ happened to rest on it. **Not supported yet:** the row/column context menu needs a long press, which some mobile browsers answer with their own menu. -### 👆 Copy, paste and fill down with a finger +### Copy, paste and fill down with a finger With a mouse these are `Ctrl+C`, `Ctrl+X`, `Ctrl+V`, `Ctrl+D` and `Delete`. A phone has no keyboard to press them on and no right-click, and in the blind test "copy *yes* down ten rows" **could not be done at all**. There @@ -2187,7 +2167,7 @@ is now a row of five named commands — **Copy · Cut · Paste · Fill down · C **Not supported yet:** pasting the system clipboard (text copied in another app) from this row — mobile browsers ask permission every time, so it uses the app's clipboard only. -### 🎯 Selecting a range with a finger without losing the data +### Selecting a range with a finger without losing the data In blind test round 2, on a phone, almost every way of selecting a range **ran on to column J** and scrolled the view to H–J, taking the data off the screen. Getting back took five to seven swipes, one column each, and a @@ -2212,11 +2192,11 @@ formatting rule made that way covered a range far wider than meant (#127). Now: **`=` then a tap on another cell no longer saves over it (#99, sev:critical).** It used to **save `=` over the cell being edited** — on the sample sheet it wrote over the word "Mains". On a touch screen that tap now -[puts the cell's address into the formula](#-tapping-cells-into-a-formula-on-a-phone). With a mouse, while a +[puts the cell's address into the formula](#tapping-cells-into-a-formula-on-a-phone). With a mouse, while a formula is still waiting for an address (`=`, `=SUM(`, `=A1+`), a click on the grid **does nothing**; a finished formula such as `=SUM(A1:A3)` is still saved by clicking another cell, as before. -### 👉 Tapping cells into a formula on a phone +### Tapping cells into a formula on a phone On a phone the only way to write a formula used to be typing every address, on a keyboard that keeps brackets and `:` two layers down. Now, while the cell's editor or the formula bar holds a formula (starts with `=`): @@ -2257,7 +2237,7 @@ formula will land in gets a green outline. **Use C2:C10** opens the form again w the caret follows an operator — later work); for now the click does nothing, so nothing is saved over. The pointed cell's dot widens only from the first cell tapped. Not yet tried on a real iPhone (the e2e drives Chromium). -### 📲 Tablets and folding phones +### Tablets and folding phones On a Galaxy Z Fold7 opened out, the toolbar **scrolled off the edge of the screen** — a width wider than a phone and narrower than a laptop, and the layout had only two shapes: below 640px, and everything else. At 1024px the @@ -2266,7 +2246,7 @@ boundary picked at a width **measured to fit in both languages**: | Width | What you get | |---|---| -| **Below 1024px** (phones, a Fold opened out, tablets upright) | The [phone](#-works-on-a-phone) layout: a named tab bar, the menu as a sheet, cell tools as a sheet | +| **Below 1024px** (phones, a Fold opened out, tablets upright) | The [phone](#works-on-a-phone) layout: a named tab bar, the menu as a sheet, cell tools as a sheet | | **1024–1365px** (tablets sideways, small laptops) | Named panel buttons in the top row · **the menu drops as a panel under its button** · the formatting row keeps B I U, alignment and colour, and **Tools** opens a panel under the bar · from **1280px**, **Import file** and **Export Excel** are back in the row | | **1366px and up** | Everything in the row, as before | @@ -2277,7 +2257,7 @@ single pixel**, and `check:a11y` checks for sideways scroll at five widths on ev four file buttons into the menu below 1366px, and e2e went red: at 1280px, the most common laptop width, every export would have cost two presses. There was room for the two that get used, so those two came back. -### 📐 One message at a time, and short screens +### One message at a time, and short screens On a phone after opening a file, the "saved in this browser only" bar (three lines) and the "file opened · Undo" bar stacked to about 170px and left **10 rows** of grid at 390×844. A phone on its side, or a screen at 200% zoom, showed @@ -2289,7 +2269,7 @@ stacked to about 170px and left **10 rows** of grid at 390×844. A phone on its - **The file-open message floats over the foot of the grid** rather than pushing it down, and goes after 10 seconds or at the next edit. Only its buttons take a touch, so a swipe that starts on its words still scrolls the grid under it. - **"Saved in this browser only"** is one line in the visit of the first edit, and then moves - [behind the save status](#-autosave--undoredo). + [behind the save status](#autosave--undoredo). - **Under 500px tall** (a phone on its side, 200% zoom) the bars fold together: - the top bar and the formula bar share one row (from 640px wide); - the format bar starts folded, and the brush on the formula bar, **"Cell format"**, opens it (the same name as its @@ -2313,7 +2293,7 @@ checks it); Redo, the language and the save status are past a scroll of the top covers two or three rows while it shows · the blocked sideways swipe from the usability test **no longer reproduces on main** (the tested build predates #142), so an e2e guards it instead. -### 🖱️ Right-click on a cell +### Right-click on a cell Right-clicking a cell used to give **the browser's own menu** (Back · Reload · Inspect); the app's menu lived only on the row and column headers, which is not where someone from Excel looks first. A cell now has its own: @@ -2329,7 +2309,7 @@ only on the row and column headers, which is not where someone from Excel looks rather than wherever the mouse is - While a cell is being edited, right-click is still **the browser's menu**, because that is where spelling and paste-as-text live -### 📏 Drag a column wider or narrower +### Drag a column wider or narrower **Drag the right edge of a column header** to resize it · **double-click the edge** to fit the longest thing in the column, as in Excel. The width is kept on the sheet, survives a reload and goes out in the `.xlsx` · one @@ -2345,7 +2325,7 @@ drag is one undo step, not one per pixel. **Not supported yet:** resizing from the keyboard, and dragging a row taller (heights from an imported file still show in full). -### ➕ Insert/delete rows & columns +### Insert/delete rows & columns Right-click a row/column header to insert or delete. The app **automatically rewrites every formula in the sheet to reference the new correct positions**; a formula that referenced the exact row/column that got deleted @@ -2354,7 +2334,7 @@ turns into `#REF!`, exactly like Excel.

Insert a row at 3 and =SUM(C2:C4) becomes =SUM(C2:C5) by itself — no chasing formulas by hand

-### 🔗 Merging cells +### Merging cells A report title spanning the width of a table is almost always a merged cell. This app already **read** merges out of an imported file and wrote them back on export — it just couldn't make one. Select a range @@ -2392,7 +2372,7 @@ stays), totals match Excel, and the merge still goes back out with the export. **Not supported yet:** vertical centring inside a merged cell, and dragging a selection out from a merged cell still measures from its top-left corner. -### 🔤 Sort and filter +### Sort and filter - **Sort** (A-Z/Z-A): selecting a single cell auto-detects the surrounding table bounds, and **the header stays on top whichever way you sort** (#49). The top row is a header when it holds no numbers and something says it is a @@ -2423,7 +2403,7 @@ list of words** (one column, nothing bold) has nothing to set its first row apar just the rows to sort to be exact (Excel guesses the same way). Bold, colours and number formats still reach hidden rows: no value is lost, but their look changes. -### 🧮 Pivot (summarise a range) +### Pivot (summarise a range) Select a range whose first row is the header, press **"Summarise (Pivot)"**, then choose which columns to group by (more than one is fine), which column to fan out across the top, and what to summarise — sum, count, @@ -2479,7 +2459,7 @@ button that does nothing. **Not supported yet:** refreshing is manual, not automatic; a refresh overwrites anything typed into the summary sheet; one column field at a time; and the summary sheet has no filters of its own. -### 📑 Multiple sheets in one file +### Multiple sheets in one file Switch/add/rename/delete sheets from the tab bar below the grid. Each sheet has independent data, formulas, and formatting, but **undo/redo and autosave cover every sheet together**. @@ -2500,12 +2480,12 @@ formatting, but **undo/redo and autosave cover every sheet together**. - **What changes, on purpose:** where two tabs shared a name, formulas used to read the *later* one and now read the first. Those formulas' values change, from the tab made afterwards back to the tab they meant. -### 📥 Import an existing Excel file +### Import an existing Excel file Reads every cell's value plus its **original formulas** into the grid and recomputes everything immediately; a multi-sheet file imports as separate tabs. The file's **look** comes too — colour bands, font sizes, borders, -row heights, merged cells — see [It looks like the file you opened](#-it-looks-like-the-file-you-opened), and -if the file was built as a form, [Templates from an Excel file](#-templates-from-an-excel-file). +row heights, merged cells — see [It looks like the file you opened](#it-looks-like-the-file-you-opened), and +if the file was built as a form, [Templates from an Excel file](#templates-from-an-excel-file). **A formula filled down or across in Excel stays a formula in every cell.** Excel stores a filled range as a *shared formula* — the first cell holds the text, the rest only point back at it — and each @@ -2525,7 +2505,7 @@ than stretching the sheet to a million. > ceiling allows, and **the app says so at once** — which sheet, the row the file reaches, and how many > rows were opened. Nothing is cut silently. -### 🧾 Told up front what a file can't keep +### Told up front what a file can't keep The people this app is for **open a file, fix it and send it back**, and the file has to come back unbroken (#83). When a file holds something the app cannot keep, such as a picture or an Excel chart, exporting and saving over the original @@ -2562,7 +2542,7 @@ package again as it opens and **says so before anything is edited**: what, how m - The report is not saved in the browser; after a reload it cannot be opened again (the original file says it again every time it is opened). -### 🛟 Opening a file without losing your work +### Opening a file without losing your work **Importing used to replace the whole workbook at once, without a word.** In the blind test someone opened a file to look up one number and lost nearly an hour of typing. Now, when there is work open, the app **asks first**: @@ -2580,7 +2560,7 @@ whole import back in one press (`Ctrl+Z` works too). It goes after 10 seconds or top bar does the same job either way; it used to be a bar that pushed the grid down (#129). **It does not ask** when the screen holds only the sample or empty sheets — a question with nothing at stake is the one that teaches people to click through without reading. -### 🆕 A blank start, and New file +### A blank start, and New file

A first visit: a blank sheet, and the green bar with Try it with sample data

@@ -2606,7 +2586,7 @@ the way columns are: a new sheet has 30, and "+ Row" adds more. A template with extra columns, since it cannot grow. Undo lives in this tab: close it and replaced work does not come back, which is why export is offered first. -### 🧩 Drag-and-drop formulas +### Drag-and-drop formulas Search/filter by category (Math / Statistics / Logic / Text / Date / Lookup), then **drag** or **click** a formula card to open the parameter panel, complete with a crosshair button to click/drag-select cells from the @@ -2627,7 +2607,7 @@ hardly anyone uses.
-### 📤 Export +### Export - **Excel**: a single `.xlsx` with **every sheet** included — original formulas, formatting (fills, font sizes, borders, row heights, column widths, merged cells) and a template's locking all intact, so it opens in @@ -2637,13 +2617,13 @@ hardly anyone uses. recalculate — a LINE or email preview, a phone's file viewer — shows that value as it is, so it shows the real number rather than 0. A test unzips the export and reads the `` in the XML directly. · **The sheet names in the file are the names in the app**, because the app only takes names Excel takes (see - [Multiple sheets in one file](#-multiple-sheets-in-one-file)), and an export that fails says so instead of + [Multiple sheets in one file](#multiple-sheets-in-one-file)), and an export that fails says so instead of doing nothing. · **Not supported yet:** newer functions (`XLOOKUP`, `SORT`, `FILTER`, `UNIQUE`…) are written without the `_xlfn.` prefix Excel may expect, so Excel may show `#NAME?`. Not fixed because it could not yet be checked against Microsoft's documentation, and guessing is worse ([#156](https://github.com/SuruchBoss/ExcelToGo/issues/156)). - **CSV**: the currently open sheet, as computed values, with the BOM Excel needs to read Thai - (see [CSV in and out](#-csv-in-and-out)). + (see [CSV in and out](#csv-in-and-out)). - **PDF**: the currently open sheet only, showing computed values with row/column headers, with the charts following underneath · **cells keep their fill and text colour** (every cell used to print white, so a sheet with coloured heading bands came out looking unformatted). A heading row given a pale fill @@ -2687,7 +2667,7 @@ loose sheets with nothing on them to say which came first. defaults are right often enough that the dialog would mostly be a thing to click through. Margins, a chosen scale and a print range are the parts a dialog would add, and they are not here. -### 🔀 CSV in and out +### CSV in and out CSV is the format every other tool speaks — a bank statement, a POS export, the file a colleague mails you. Opening one here used to mean opening it in Excel first and saving it as `.xlsx`, which made this app the long @@ -2721,7 +2701,7 @@ there's no encoding detection. And a field that genuinely began with an apostrop else's file is indistinguishable from one this app escaped, so it loses that apostrophe on the way in. CSV has no way to say "text that happens to look like a formula", so something has to give. -### 🎨 It looks like the file you opened +### It looks like the file you opened

@@ -2748,7 +2728,7 @@ All of it **exports back to `.xlsx`**, so the file opens in Excel as the file it > (the system font is used), and arrowing into a cell a merge has swallowed — that cell no longer > exists in the DOM, though the merged band itself clicks normally. -### 📋 Templates from an Excel file +### Templates from an Excel file

@@ -2788,9 +2768,9 @@ still a form. > already are one. (Merged cells and conditional formatting were both on this list once; both have > since shipped.) -### 📊 A usage count that provably cannot identify anyone +### A usage count that provably cannot identify anyone -The smallest useful question is **"has anyone actually used this, or am I looking at my own +The smallest useful question is **"has anyone actually used this, or are these the maintainer's own visits?"**, and it sits against the sentence the landing page leads with — so the shape of the answer is the argument, not a footnote to it. @@ -2874,7 +2854,7 @@ wrong `bad_key`. *people* (two visits from one person and one each from two are the same number), whether anyone came back, where they came from, or anything at all about a single visit. -### 🏠 A landing page that explains the app +### A landing page that explains the app `localhost:3000` now opens on a page describing what the app does, with an **"Open the app"** button through to `/app` — where before you landed straight in a bare grid with nothing telling you how to use it. Every @@ -2902,7 +2882,7 @@ outright is one nobody believes. recall", "mistyped cell addresses" — capabilities dressed up as problems, which nobody reads and thinks *that is me* — so it became situations told as situations. But problems and features still lived in separate sections: the problems had no pictures, and the features never said what they were for. Now -they are one: **[six problems a team already pays for](#-problems-it-solves)**, each written as a ledger +they are one: **[six problems a team already pays for](#problems-it-solves)**, each written as a ledger entry — who has it · **what it costs now, in red ink** (the only place the page uses that colour for words) · the features that solve it as lines a, b, c, because one problem is usually solved by several working together · and **the outcome under a double rule**, the way an account closes its total. @@ -2960,7 +2940,7 @@ email and LinkedIn written as two lines of the ledger like everything above them on webmail a `mailto:` link does nothing at all; the copy result is announced in a live region. The footer gains an email link next to LinkedIn and GitHub as well. -### ❓ Questions people ask, on the landing page +### Questions people ask, on the landing page

The landing page's questions section: seven questions, each answered in its first sentence and followed by its limits

@@ -2980,7 +2960,7 @@ differs from Excel and Google Sheets. the function counter had been one short all along** (`"RANK.EQ"` is quoted and the old pattern skipped it), so the 65 on the landing page, the README and the link-preview card is now 66. -### 📘 One page per formula +### One page per formula

The SUMIF page, in Thai: the sentence that answers what it does, its shape with each argument explained, and an example table with the northern branch's rows highlighted and the 450 total worked out by the app's engine, with a Try it in the sheet button

@@ -3008,21 +2988,21 @@ buttons, from the FAQ's fifth question, and from the end of the guide. - **Limits:** these pages are Thai only until they have English addresses of their own, which is why the picture above is in Thai. The rest of the palette's formulas are still explained only in the app (the list page counts them from the palette). -### 🌐 Bilingual (Thai / English) +### Bilingual (Thai / English) Click **EN**/**ไทย** in the top-right corner to switch the entire UI instantly — menus, buttons, all 37 formula names/descriptions, alert text, and AI replies (both the keyword heuristic and real Claude) all follow the selected language. The choice is remembered per browser · **On a first visit the language follows the browser** (Thai anywhere in the browser's language list → Thai, otherwise English). Everyone used to land in Thai, so a reader who could not read it had to find the EN button on a page they could not read · `` -and the tab title follow too, so a screen reader reads in the right language (the title used to stay Thai on an English browser, because React wrote the metadata title back after load — a guard now puts it back; found by QA round 2). See [Bilingual UI (i18n)](#-bilingual-ui-i18n) for the +and the tab title follow too, so a screen reader reads in the right language (the title used to stay Thai on an English browser, because React wrote the metadata title back after load — a guard now puts it back; found by QA round 2). See [Bilingual UI (i18n)](#bilingual-ui-i18n) for the architecture behind it.

The same sample sheet after one click on TH: every label in Thai

--- -## 🛠 Tech stack +## Tech stack ### Framework / language @@ -3049,7 +3029,7 @@ architecture behind it. > **Note:** No off-the-shelf formula library (e.g. HyperFormula) is used — the **formula engine is hand-written** > (tokenizer, parser, evaluator, and functions) to keep full control over its behavior. See -> [Formula engine](#-formula-engine) for details. +> [Formula engine](#formula-engine) for details. ### Tooling @@ -3060,7 +3040,7 @@ architecture behind it. --- -## 🏛 Architecture +## Architecture ### System overview @@ -3192,7 +3172,7 @@ formulaEngine/ (tokenizer → parser → evaluator → functions) --- -## 📁 Project structure +## Project structure ``` src/ @@ -3457,7 +3437,7 @@ one place: `store/sheetStore.ts`. --- -## 🧮 Formula engine +## Formula engine The most deliberately-built part of the project, because it's the one place a mistake fails silently — a wrong number, with no error shown. @@ -3605,7 +3585,7 @@ The drag-and-drop palette shows only the **37 most commonly used** formulas, but > **The last ten came from asking the real model, not from working through the Excel reference.** > `TEXTJOIN` `FIND` `RANK.EQ` `SUMPRODUCT` `CEILING` `CHAR` — and their obvious companions `SEARCH` > `SUBSTITUTE` `FLOOR` `CODE` — are what the assistant answered with while this engine had no such -> function. See [Ask AI for a formula](#-ask-ai-for-a-formula) for how that was found. +> function. See [Ask AI for a formula](#ask-ai-for-a-formula) for how that was found. **`INDEX` + `MATCH` replaces `VLOOKUP` and does what it cannot** — `VLOOKUP` can only search the leftmost column of a table, and hard-codes *which column number* to return, which breaks silently @@ -3636,7 +3616,7 @@ The nearest-match modes compare rather than assume the column is sorted, which i same length: Excel would spill a whole row out of a two-dimensional return array, and `XLOOKUP` here does not, so that is refused rather than answered with the first cell. (This line used to read "this engine has no spilling", which was true when it was written and stayed -there after [array formulas](#-formulas-that-answer-with-a-whole-table-array-formulas) shipped — +there after [array formulas](#formulas-that-answer-with-a-whole-table-array-formulas) shipped — spilling exists now; `XLOOKUP` simply has not been wired to it.) **An argument can be left out mid-formula** — `XLOOKUP(a,b,c,,-1)` skips `if_not_found` to reach the @@ -3753,7 +3733,7 @@ plus both languages' text in `formulaCatalog.ts` and `i18n/th.ts`/`en.ts`. --- -## 🌐 Bilingual UI (i18n) +## Bilingual UI (i18n) No off-the-shelf i18n library (e.g. next-intl) is used, since the app is already entirely client-rendered and doesn't need locale-based routing (`/en/...`) — instead it's a hand-written dictionary, with **TypeScript @@ -3791,9 +3771,9 @@ flowchart LR --- -## 🔐 Security — what was actually tested +## Security — what was actually tested -This section exists because it is the line between "I asked an AI for an app" and building software. +This section exists because it marks the difference between asking an AI for an application and building software. Code that looks right and runs is not the same as code that is safe, and the only way to find out is to attack it. @@ -4007,11 +3987,11 @@ nothing would warn you if a future change broke them: not survive because this app's own engine had already evaluated it — **the dangerous prefixes are exactly the three the engine does not treat as a formula**, so "we compute formulas ourselves" was never a protection. It has its own security tests in `csvInjection.test.ts` (see - [CSV in and out](#-csv-in-and-out)). + [CSV in and out](#csv-in-and-out)). - **There are no user accounts**, so there is no per-user authorisation to test. `SOURCES_ADMIN_TOKEN` is an operator switch, not an account. -## 📈 Lighthouse +## Lighthouse Measured against a local production build (`npm run build && npm run start`) with Lighthouse 12 on the **mobile** preset — 4x CPU slowdown and simulated slow 4G, not a desktop run dressed up as one. @@ -4030,7 +4010,7 @@ between any two attempts; the +10–15 ms of server time the nonce costs does no LCP of three and a half seconds. A number in a README that was true on an older build is the failure this project has already had twice, so it was cheaper to run it again than to argue. -**Accessibility 100 on both pages**, which agrees with the [`check:a11y`](#-testing) gate that runs axe on +**Accessibility 100 on both pages**, which agrees with the [`check:a11y`](#testing) gate that runs axe on every PR — two different tools, same answer. **Both of them were also wrong about the grid, and worth saying so.** Lighthouse 100 and axe clean at @@ -4098,9 +4078,9 @@ mostly Next's own framework chunks that aren't needed for the first frame. Going the framework bundle itself, which isn't a trade worth making here. Written down rather than rounded up. > These are localhost numbers, not Vercel's. The deployed site has a CDN and compression and should do -> better, but I can't verify that from here, so only what was actually measured is reported. +> better, but that has not been measured, so only the figures actually measured are reported. -## 🧪 Testing +## Testing ```bash npm test # 2017 cases across 145 files, via Vitest @@ -4225,14 +4205,14 @@ measurement. **Each gate was proved by breaking it first** — take `say(...)` out of `addRow` and the fifth flow fails at once; disable `ArrowRight` in the grid and two assertions in the third fail. (The first attempt at that -second one stayed green: the `case` I inserted landed *after* the existing `case "ArrowRight"` and was dead +second one stayed green: the inserted `case` landed *after* the existing `case "ArrowRight"` and was dead code. Proving a gate means checking that the thing you meant to break actually broke.) -> **2017 tests passed, and 43% of the assistant's answers were unusable** — because those tests mock -> the model, so it returns what the test author imagined. A test count says what you thought to ask, -> not whether you asked enough. Only a real API key found this: see -> [What 2017 passing tests could not catch](#-what-2017-passing-tests-could-not-catch), repeatable -> with `npm run check:ai`. +> **All 2017 tests passed, yet 43% of the assistant's answers were unusable**, because those tests mock +> the model and so return what the test author expected. A test count shows what was tested, not whether +> the testing was sufficient. Only a real API key revealed this; see +> [Limits of automated testing](#limits-of-automated-testing). The check can be repeated with +> `npm run check:ai`. | File | Cases | Tests | |---|---|---| @@ -4345,21 +4325,55 @@ as the one floor. and because the range does not reach the next major, Vercel will not quietly move to it. A new Node major goes into CI and `engines` together. +### Limits of automated testing + +Every test of the assistant **mocks the model**, so it answers as the test's author expected. With a real API key +and fourteen ordinary questions, **six answers used functions this engine does not support** (`TEXTJOIN`, `FIND`, +`RANK.EQ`, `SUMPRODUCT`, `CEILING`, `CHAR`). Each was valid Excel, and each showed `#NAME?` in the cell after the +user pressed a button labelled "insert". + +The first fix made the problem worse. The system prompt asked for "the closest formula the list allows", so +_"join all the names into one line"_ returned `=SUM(A2:A20)`, which shows `0` with no warning. **A visible error +had been replaced by an invisible wrong number.** + +The same pattern appeared again elsewhere in the app. With every gate passing (the full suite green and `axe` +reporting no violations on either page at either width), about an hour of using the public build as a first-time +visitor found three problems that no gate detected. + +- With a formula from the palette half-inserted, **all six sidebar buttons stopped responding.** They still set the + mode in the store, but the render condition `pending ? … : mode` gave precedence to `pending`, and Escape did + not clear it, so the pressed command took effect later, together with whichever click cancelled the formula. +- The keyword matcher, which on the public site without a key is the only answer a visitor sees, answered + _"add up all the sales"_ with `=SUM(E2)`, the total of a single cell, and _"join the product name and the + category"_ with the same formula. The system prompt had forbidden that substitution for the model since the + fix above, but the rule had never been applied to the matcher. +- `axe` passed the grid in all four combinations (two pages at two widths), while to a screen reader it was an + ordinary table that could not be operated: no `role="grid"`, no `aria-selected`, no `scope` on the headers, + every cell reachable with Tab, and **browser focus remaining on A1 while Ctrl+↓ moved the cursor to A10**. + The `axe` result was correct, since a `` with `` in the DOM. Still open: editing the cell a whole column of running totals reads still costs around 1,000–1,200 ms (a real fan-out, not a cache miss), and columns are not windowed, only rows. -- [x] **Full Excel keyboard coverage** — done (see [the Excel keyboard](#️-the-excel-keyboard)): +- [x] **Full Excel keyboard coverage** — done (see [the Excel keyboard](#the-excel-keyboard)): `Ctrl+arrow` to the edge of the data, `Shift+arrow` to drag the selection, both together for each at once, `Home`/`End`/`Ctrl+Home`/`Ctrl+End`, Page keys measured in pixels, the two-step `Ctrl+A`, and the view following the cursor in both axes. **`Ctrl+Space` / `Shift+Space` for a whole column or row and `Ctrl+Enter` to fill a selection are all in now** — and the fill shifts references the way a drag does, since a formula that kept pointing at the anchor's row would fill a column with the same wrong number. Still open: freezing panes beyond the sticky header. -- [x] **Data validation** — done (see [Data validation](#-data-validation)): a list, a number range or a +- [x] **Data validation** — done (see [Data validation](#data-validation)): a list, a number range or a length cap, refused before it is written and announced, moving with row edits and round-tripping through `.xlsx`. Still open: "date between" and custom formulas, and a list containing a comma cannot be written to the file. -- [x] **Named ranges** — done (see [Named ranges](#-named-ranges)): Thai names work, substitution happens +- [x] **Named ranges** — done (see [Named ranges](#named-ranges)): Thai names work, substitution happens at compile time so the dependency graph stays honest, names follow row edits, and they round-trip through `.xlsx`; since #60 a name is the workbook's, as in Excel, with a file's sheet-level names kept. Still open: choosing a scope when making a name in the app, formula and whole-column names, and a name box beside the formula bar to jump to a range. -- [x] **Database sources (Postgres/MySQL)** — done (see [Straight into a database](#-straight-into-a-database-postgresql--mysql)): +- [x] **Database sources (Postgres/MySQL)** — done (see [Straight into a database](#straight-into-a-database-postgresql--mysql)): tech saves a connection string and a query once, users only ever see the table, and the statement runs in a read-only transaction. Still open: a table picker instead of typed SQL, and a test that connects to a real database — today only the pure modules around it are covered. @@ -4635,43 +4649,43 @@ What's not done yet, and why — to show this is a known gap, not something forg they were dialogs - [x] **Leading zeros stay (#23)** — phone numbers, ID cards and codes are text from the moment the sheet computes, by one rule the `.xlsx` writer shares (see - [Phone numbers and codes keep their zeros](#-phone-numbers-and-codes-keep-their-zeros)) + [Phone numbers and codes keep their zeros](#phone-numbers-and-codes-keep-their-zeros)) - [x] **`SUM`/`AVERAGE`/`COUNT` skip text read from cells, as Excel does (#38, #166)** — done, for ranges and single cells alike, and a name nothing defines is `#NAME?` everywhere - [x] **Live CSV data uses the same rule (#36)** — done: leading zeros in live CSV data stay -- [x] **Sorting keeps row formulas right, and asks before a sort that would not (#48)** — done (see [Sort and filter](#-sort-and-filter)) · still open: a formula outside the range pointing at one cell inside it -- [x] **Percent ×100 as Excel does, and a file's number formats shown as the file has them (#53)** — done (see [Cell formatting](#-cell-formatting)) · typing `50%` as a number (#52) is done too · still open: writing codes in the app +- [x] **Sorting keeps row formulas right, and asks before a sort that would not (#48)** — done (see [Sort and filter](#sort-and-filter)) · still open: a formula outside the range pointing at one cell inside it +- [x] **Percent ×100 as Excel does, and a file's number formats shown as the file has them (#53)** — done (see [Cell formatting](#cell-formatting)) · typing `50%` as a number (#52) is done too · still open: writing codes in the app - [x] **Numbers from outside are right (#52, #55)** — done: `1,250` `15%` `฿1,500.00` typed or pasted are numbers with a matching format, codes stay text, and a merge in an Excel file holds its value in the top-left cell only, so totals match Excel -- [x] **Dates are dates (#45)** — done (see [Dates and times](#-dates-and-times)): Excel serials, times kept +- [x] **Dates are dates (#45)** — done (see [Dates and times](#dates-and-times)): Excel serials, times kept on import, real date cells on export, a file's own layout shown as the file has it, and `###` rather than a cut-off date -- [x] **Opening blank, a sample button, and New file** — done (see [A blank start, and New file](#-a-blank-start-and-new-file)) +- [x] **Opening blank, a sample button, and New file** — done (see [A blank start, and New file](#a-blank-start-and-new-file)) - [ ] **Date formulas formatted for you** — only `TODAY`/`NOW`/`DATE` today; `=A1+30` shows a number until a format is picked - [ ] **Date validation rules** — import and export Excel's date validation (the ERP template uses it); dropped on open today -- [x] **Thai dates (#82)** — done (see [Buddhist-Era dates](#-buddhist-era-dates)): Buddhist-Era years and Thai months read +- [x] **Thai dates (#82)** — done (see [Buddhist-Era dates](#buddhist-era-dates)): Buddhist-Era years and Thai months read as Gregorian dates, a Convert to dates command for `15/01/69` and Gregorian `dd/mm/yyyy`, a "Date (B.E.)" format that round-trips through `.xlsx` - [ ] **Try a file Thai-locale Excel saved itself** — both opening one whose dates carry a B.E. format and the `[$-107041E]d/m/yyyy;@` code going out are still uncompared with a real file, waiting on a sample (see - [the Buddhist-Era limits](#-buddhist-era-dates)) + [the Buddhist-Era limits](#buddhist-era-dates)) - [ ] **Thai digits and times written `13.45 น.`** — not read as dates or times yet - [ ] **A CSV field starting with `'`** — on import the first `'` is read as Excel's "the rest is text" marker and not shown. A file that means the apostrophe literally (a name like `'s-Hertogenbosch`) loses it for now -- [x] **The blind usability pass** — [opening a file without losing your work](#-opening-a-file-without-losing-your-work) - with an undo, [touch commands on a phone](#-copy-paste-and-fill-down-with-a-finger), [fill colour](#-fill-colour), - `Ctrl+B/I/U`, a first language taken from the browser, [a tablet and folding-phone layout](#-tablets-and-folding-phones), +- [x] **The blind usability pass** — [opening a file without losing your work](#opening-a-file-without-losing-your-work) + with an undo, [touch commands on a phone](#copy-paste-and-fill-down-with-a-finger), [fill colour](#fill-colour), + `Ctrl+B/I/U`, a first language taken from the browser, [a tablet and folding-phone layout](#tablets-and-folding-phones), a reordered AI panel and a Common formulas group · what the same pass found and is still open is below - [x] **`Tab` then `Enter` returns to the starting column**, as in Excel — done, both while editing and with a cell - only selected (see [The Excel keyboard](#️-the-excel-keyboard)) + only selected (see [The Excel keyboard](#the-excel-keyboard)) - [x] **Drag a column header's edge to resize it** — done, with double-click to fit (see - [Drag a column wider or narrower](#-drag-a-column-wider-or-narrower)). Still open: resizing from the keyboard, and dragging a row taller -- [x] **A right-click menu on cells on a desktop** — done, and `Shift+F10` opens it from the keyboard (see [Right-click on a cell](#️-right-click-on-a-cell)). Still open: comments and formatting in the menu + [Drag a column wider or narrower](#drag-a-column-wider-or-narrower)). Still open: resizing from the keyboard, and dragging a row taller +- [x] **A right-click menu on cells on a desktop** — done, and `Shift+F10` opens it from the keyboard (see [Right-click on a cell](#right-click-on-a-cell)). Still open: comments and formatting in the menu - [x] **Keep the phone keyboard up after Enter** — done: Enter goes into the next cell ready to type (see - [Works on a phone](#-works-on-a-phone)) · tested in Chromium with touch emulated; still to try on real iOS and Android + [Works on a phone](#works-on-a-phone)) · tested in Chromium with touch emulated; still to try on real iOS and Android - [x] **The connect-an-API, source settings and data picker (#122) dialogs close on `Escape` now** — through `useDialogKeys`, the same hook as the import dialog: Escape closes · Tab stays inside · focus goes in on open and back to the button on close - [x] **Fills in the PDF export** — done, with text colour. Still open: bold and italic in the PDF - [ ] **Pasting the system clipboard** from the phone command row - [x] **A shorter landing page for people who are not developers** — done, a phone went 13.9 → 7.6 screens (see - [A landing page that explains the app](#-a-landing-page-that-explains-the-app)). Still open: try it on people who + [A landing page that explains the app](#a-landing-page-that-explains-the-app)). Still open: try it on people who are not developers — whether they open the folded cards, or the proof screenshot should sit outside the fold - [ ] **Push-based realtime (SSE/WebSocket)** instead of polling, and filtering live data from the UI before placing it - [ ] **Working with PaynEat ERP** — agreed, not built (see [docs/payneat-erp.en.md](docs/payneat-erp.en.md)): @@ -4682,16 +4696,16 @@ What's not done yet, and why — to show this is a known gap, not something forg **Deliberately out of scope:** - **A hosted collaboration service** — collaboration itself *shipped*, on the user's own backend (see - [Editing together](#-editing-together)). Running one central server for everyone is the part that + [Editing together](#editing-together)). Running one central server for everyone is the part that stays out of scope: it would reverse the property the whole app stands on — your file never leaves your browser. - **Using an off-the-shelf formula library** — the engine is hand-written on purpose to keep full control over - its behavior (see [Formula engine](#-formula-engine)), even at the cost of fewer built-in functions than a + its behavior (see [Formula engine](#formula-engine)), even at the cost of fewer built-in functions than a library like HyperFormula would offer. --- -## 👤 Author +## Author Built by **Suruch Boss** @@ -4699,11 +4713,11 @@ Built by **Suruch Boss** - LinkedIn — [linkedin.com/in/suruchboss](https://www.linkedin.com/in/suruchboss) - Email — [bossxiii@gmail.com](mailto:bossxiii@gmail.com) -If this project is useful to you, or you'd like to talk about work, do get in touch. +For questions about the project or enquiries about work, please use the contact details above. --- -## 📄 License +## License Released under the [Apache License 2.0](LICENSE) — free to use, modify and use commercially. diff --git a/README.md b/README.md index 54014055..f3d6d8ec 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,11 @@ -# 📊 ExcelToGo — พิมพ์บอกเป็นภาษาไทย แล้วได้สูตร Excel แนะนำ +# ExcelToGo -**ภาษา:** ไทย · [English](README.en.md) - -### ▶ [ลองใช้เลย — ไม่ต้องติดตั้ง](https://excel-to-go.vercel.app) - -**สถานะ (ก.ย. 2026):** กำลังปิดงานชุดสุดท้ายก่อนพักการเพิ่มฟีเจอร์ใหม่ ([รายการ](https://github.com/SuruchBoss/ExcelToGo/issues?q=label%3Astatus%3Asprint-final)) · หลังจากนั้นแอปยังเปิดให้ใช้ฟรีเหมือนเดิม และยังรับ[รายงานบั๊ก](https://github.com/SuruchBoss/ExcelToGo/issues) กับดูแลเรื่องความปลอดภัย ([SECURITY.md](SECURITY.md)) +**เว็บแอปตารางคำนวณที่ช่วยเขียนสูตร Excel จากคำอธิบายภาษาไทย และประมวลผลทั้งหมดในเบราว์เซอร์** -เปิดในเบราว์เซอร์ได้ทันที ข้อมูลอยู่ในเครื่องคุณเท่านั้น -เว็บนี้คือแอปตัวจริง ใช้ฟรี ไม่ต้องสมัคร ไม่ใช่ตัวทดลอง — และ[ต่อ API ของคุณเองจากเบราว์เซอร์](#ต่อจากเบราว์เซอร์-restcsv--ใช้บนเว็บนี้ได้เลย)ได้เลย -โดย URL, header และข้อมูลไม่ผ่านเซิร์ฟเวอร์ของเรา +**ภาษา:** ไทย · [English](README.en.md) -> **พิมพ์ว่า "รวมยอดขายเฉพาะสาขาเหนือ" แล้วได้สูตร Excel แนะนำกลับมา** พร้อมคำอธิบายว่ามันทำอะไร -> กดปุ่มเดียวใส่ลงเซลล์ ไม่ต้องจำว่า SUMIF รับพารามิเตอร์อะไรก่อนหลัง — หรือถ้าไม่อยากพิมพ์ **เลือกจากสูตร -> พร้อมใช้ 37 แบบด้วยการลากวาง** แล้วลากเลือกช่วงเซลล์แทนการพิมพ์ที่อยู่เอง -> -> **ไม่ต้องใส่ key ก็ลองได้ — แอปเดาสูตรจากคำสำคัญ และบอกทุกครั้งว่าเป็นการเดา ตรวจก่อนใช้** (ทางนี้คำถาม -> ที่อยู่ช่วง และหัวคอลัมน์ถูกส่งไปเดาที่เซิร์ฟเวอร์ของเว็บนี้) · ใส่ Anthropic -> API key ของคุณเองเพื่อให้ AI (Claude) ตอบแม่นขึ้น — key อยู่ในเบราว์เซอร์คุณ คำขอวิ่งจากเบราว์เซอร์ไปหา -> Anthropic โดยตรง ไม่ผ่านเซิร์ฟเวอร์ของแอปนี้ -> -> ทั้งหมดนี้ทำบนไฟล์ `.xlsx` ที่**เปิดมาแล้วหน้าตายังเหมือนเดิม** (แถบสี เซลล์ที่ merge เส้นขอบ ความสูงแถว) -> และคำนวณด้วย**เอนจินคำนวณสูตรที่เขียนขึ้นเอง** (ไม่พึ่งไลบรารีสำเร็จรูป) ทุกอย่างอยู่ในเบราว์เซอร์ -> ไฟล์ไม่ออกจากเครื่อง และไม่ต้องสมัครสมาชิก — ยกเว้นตอนกดถาม AI ที่คำถามกับหัวคอลัมน์ถูกส่งออกไป -> ([ส่งไปที่ไหนบ้าง](#-ถาม-ai-หาสูตร)) -> -> ต่อยอดไปอีก 4 อย่าง: **[ต่อ API ของคุณ แล้วค่าไหลเข้าเซลล์เอง](#-ข้อมูลสดจาก-api--csv-prototype)** โดยไม่ต้องเขียนสคริปต์ -> — [จากเบราว์เซอร์บนเว็บนี้ได้เลย](#ต่อจากเบราว์เซอร์-restcsv--ใช้บนเว็บนี้ได้เลย) รวมถึง API หลัง VPN หรือในออฟฟิศ หรือฐานข้อมูลหลังบ้านเมื่อรันแอปเอง -> (ไล่ดึง API ที่แบ่งหน้าและถอยเองเมื่อโดน rate limit), **[เปิดไฟล์แล้วหน้าตาเหมือนเดิม](#-เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม)** -> (แถบสี ตัวอักษรใหญ่ เส้นขอบ เซลล์ที่ merge), **[แม่แบบจากไฟล์ Excel](#-แม่แบบ-template-จากไฟล์-excel)** -> ที่รู้เองว่าช่องไหนให้กรอก และ **[สรุปข้อมูลแบบ Pivot](#-สรุปข้อมูล-pivot)** ที่จัดกลุ่มแล้วออกมาเป็นชีตใหม่ -> -> กราฟที่ส่งออกไปกับ `.xlsx` เป็น**กราฟจริงที่แก้ต่อได้ใน Excel** ไม่ใช่รูปภาพ — เขียน OOXML chart part เอง -> เพราะ ExcelJS เขียนให้ไม่ได้ +[เปิดใช้งาน](https://excel-to-go.vercel.app) · [คู่มือการต่อข้อมูล](https://excel-to-go.vercel.app/guide) · +[รายงานปัญหา](https://github.com/SuruchBoss/ExcelToGo/issues) · [นโยบายความปลอดภัย](SECURITY.md)

Next.js @@ -44,99 +18,107 @@ CI

-**English TL;DR** — Describe what you want in plain Thai or English and get a suggested Excel formula back, -with a sentence explaining it; one click puts it in the cell. **Without a key the app guesses from keywords and -always says it is a guess — check before you use it**; paste your own Anthropic API key for real AI answers -from Claude — from your browser straight to Anthropic, never through this app's server. Beyond the assistant, it is -a Next.js web app that turns an Excel-style grid into a friendlier UI: drag-and-drop -ready-made formulas instead of memorizing syntax, an AI assistant that suggests formulas from a natural-language -question (Thai or English), and a hand-written formula engine (tokenizer → parser → evaluator, no third-party -formula library) supporting cell/range references, relative & structural reference adjustment, circular-reference -detection and multi-sheet workbooks. Imported .xlsx files keep their look — colour bands, font sizes, borders, -merged cells — and a protected file is read as a fill-in template that knows which cells are yours. Cells can -also be bound to a live REST/CSV source fetched by your own browser, straight from the public site with nothing -to install and nothing passing through its server — or, self-hosted, straight to PostgreSQL/MySQL, where one saved read-only query -becomes a table and nobody downstream ever sees SQL — following paginated APIs, backing off when rate-limited, -and saying so when data came back incomplete. A range can be given a name (in Thai) so `=SUM(ยอดขาย)` reads as -what it means, and a cell can carry a rule about what may go in it, refused before it is written rather than -flagged afterwards. Conditional formatting re-colours cells from their current values — comparisons, -top/bottom ranks, colour scales and data bars — and round-trips through Excel's own rule format. Bilingual UI -(Thai/English), 2017 automated tests. บันทึกบนคลาวด์และการแก้พร้อมกันหลายคนมีให้เลือกใช้ได้ โดยต่อ backend ของคุณเอง +> [!NOTE] +> **สถานะโครงการ (ตุลาคม 2026):** งานชุดสุดท้ายก่อนพักการพัฒนา +> ([ป้าย `status:sprint-final`](https://github.com/SuruchBoss/ExcelToGo/issues?q=label%3Astatus%3Asprint-final)) +> ปิดครบเมื่อวันที่ 5 ตุลาคม 2026 การเพิ่มฟีเจอร์ใหม่จึงพักไว้ก่อน แอปยังเปิดให้ใช้งานฟรีตามเดิม +> และโครงการยังรับ[รายงานปัญหา](https://github.com/SuruchBoss/ExcelToGo/issues) และดูแลด้านความปลอดภัยตาม [SECURITY.md](SECURITY.md) + +## ภาพรวม + +ExcelToGo เป็นเว็บแอปตารางคำนวณสำหรับผู้ที่ใช้ Excel เป็นประจำแต่ไม่ได้เขียนสูตรเป็นงานหลัก +ผู้ใช้อธิบายสิ่งที่ต้องการเป็นภาษาไทยหรือภาษาอังกฤษ เช่น _"รวมยอดขายเฉพาะสาขาเหนือ"_ +แล้วได้สูตร Excel ที่แนะนำพร้อมคำอธิบายก่อนเลือกใส่ลงเซลล์ หรือเลือกจากสูตรพร้อมใช้ 37 แบบ +แล้วเลือกช่วงเซลล์จากตารางโดยตรงแทนการพิมพ์ที่อยู่เซลล์เอง + +สูตรทั้งหมดคำนวณด้วยเอนจินที่โครงการเขียนขึ้นเองโดยไม่พึ่งไลบรารีสูตรสำเร็จรูป ไฟล์ `.xlsx` ที่เปิดเข้ามา +คงหน้าตาเดิม และไฟล์ที่ส่งออกยังเก็บสูตรไว้เป็นสูตร เว็บไซต์ข้างต้นเป็นแอปฉบับใช้งานจริง ไม่ใช่ฉบับทดลอง +ใช้งานได้ฟรีโดยไม่ต้องสมัครสมาชิก + +## ความสามารถหลัก + +- **ผู้ช่วยเขียนสูตร** — [ถามเป็นภาษาไทยหรืออังกฤษ](#ถาม-ai-หาสูตร) แล้วได้สูตรพร้อมคำอธิบาย + เมื่อไม่ได้ใส่ API key แอปจะเดาสูตรจากคำสำคัญและแจ้งทุกครั้งว่าเป็นการเดาที่ควรตรวจก่อนใช้ + เมื่อใส่ [Anthropic API key ของผู้ใช้เอง](#ใส่-api-key-ของคุณเอง-byok) คำถามจะถูกส่งให้ Claude โดยตรงจากเบราว์เซอร์ +- **สูตรพร้อมใช้แบบลากวาง** — [เลือกสูตรจากรายการ](#ใส่สูตรแบบลากวาง) กรอกทีละช่อง และเลือกช่วงเซลล์จากตาราง + โดยไม่ต้องจำลำดับพารามิเตอร์ +- **เอนจินคำนวณสูตรที่เขียนขึ้นเอง** — [tokenizer → parser → evaluator](#เอนจินคำนวณสูตร) รองรับการอ้างอิงเซลล์และช่วง + การเลื่อนอ้างอิงเมื่อคัดลอกหรือแทรกแถว การตรวจจับการอ้างอิงวนกลับ หลายชีต และ[ชื่อช่วงภาษาไทย](#ตั้งชื่อช่วงข้อมูล-named-ranges) +- **เปิดและส่งออกไฟล์ Excel** — [ไฟล์ที่เปิดคงหน้าตาเดิม](#เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม) (แถบสี เซลล์ที่ merge เส้นขอบ + ความสูงแถว) และ[ส่งออก](#ส่งออกไฟล์)เป็น `.xlsx`, CSV หรือ PDF โดยกราฟใน `.xlsx` เป็นกราฟจริงที่แก้ต่อใน Excel ได้ +- **แม่แบบจากไฟล์ Excel** — [ไฟล์ที่ป้องกันไว้ถูกอ่านเป็นแม่แบบ](#แม่แบบ-template-จากไฟล์-excel) ที่รู้ว่าช่องใดให้ผู้ใช้กรอก +- **ข้อมูลสดจาก API** — [ต่อ REST/CSV API จากเบราว์เซอร์](#ต่อจากเบราว์เซอร์-restcsv--ใช้บนเว็บนี้ได้เลย)บนเว็บไซต์นี้ได้ทันที + รวมถึง API หลัง VPN หรือในเครือข่ายสำนักงาน หรือต่อ[ฐานข้อมูล PostgreSQL / MySQL](#ต่อฐานข้อมูลตรง-ๆ-postgresql--mysql) + เมื่อติดตั้งแอปเอง ค่าในเซลล์รีเฟรชตามรอบ รองรับ API ที่แบ่งหน้าและการรอเมื่อถูกจำกัดอัตรา +- **สรุปข้อมูลแบบ Pivot** — [จัดกลุ่มช่วงข้อมูล](#สรุปข้อมูล-pivot)แล้วเขียนผลเป็นชีตใหม่ พร้อมแจ้งเมื่อข้อมูลต้นทางเปลี่ยน +- **ใช้งานบนมือถือและสองภาษา** — [รองรับจอสัมผัส](#ใช้บนมือถือได้) และ[หน้าจอภาษาไทยและอังกฤษ](#สองภาษา-ไทย--english) +- **บันทึกบนคลาวด์และแก้ไขพร้อมกัน (ไม่บังคับ)** — [ต่อ backend ของผู้ใช้เอง](#บันทึกบนคลาวด์-ต่อ-backend-ของคุณเอง) + +## การจัดการข้อมูล + +ตาราง ไฟล์ที่เปิด และไฟล์ที่ส่งออก ประมวลผลในเบราว์เซอร์ของผู้ใช้ทั้งหมด ข้อมูลที่ออกนอกเครื่องมีเฉพาะกรณีต่อไปนี้ + +| กรณี | สิ่งที่ส่ง | ปลายทาง | +|---|---|---| +| ถาม AI โดยไม่ใส่ API key | คำถาม ที่อยู่ช่วง หัวคอลัมน์ และค่าที่ผู้ใช้พิมพ์ไว้ในคำถาม ([รายละเอียด](#ถาม-ai-หาสูตร)) | เซิร์ฟเวอร์ของเว็บไซต์นี้ | +| ถาม AI ด้วย API key ของผู้ใช้ | ข้อมูลชุดเดียวกัน | Anthropic โดยตรงจากเบราว์เซอร์ | +| ต่อข้อมูลสดจากเบราว์เซอร์ | คำขอไปยัง API ที่ผู้ใช้กำหนด | API ปลายทางนั้นโดยตรง โดยไม่ผ่านเซิร์ฟเวอร์ของเว็บไซต์นี้ | +| ตัวนับการใช้งาน (เมื่อผู้ดูแลเปิดใช้) | ชื่อเหตุการณ์จากรายการตายตัว ไม่มีเนื้อหาในเซลล์และไม่มีตัวระบุตัวตน ([รายละเอียด](#นับยอดใช้งานโดยพิสูจน์ได้ว่าระบุตัวใครไม่ได้)) | เซิร์ฟเวอร์ของเว็บไซต์นี้ | +| รายงานเมื่อแอปขัดข้อง (เมื่อผู้ดูแลตั้งค่า) | ข้อความ error และ stack ที่ลบ key, token, อีเมล และข้อความภาษาไทยออกแล้ว | ปลายทางที่ผู้ดูแลกำหนด | +| บันทึกบนคลาวด์ (เมื่อเลือกเปิด) | สมุดงาน | backend ที่ผู้ใช้ตั้งค่าเอง | + +**English summary** — ExcelToGo is a browser-based spreadsheet that suggests Excel formulas from a description +in Thai or English and explains each one before it is inserted. Without an API key it offers a keyword-based guess, +clearly marked as such; with the user's own Anthropic API key the question is sent from the browser directly to +Claude. Formulas are computed by a hand-written engine (tokenizer → parser → evaluator, no third-party formula +library). Imported `.xlsx` files keep their formatting, exported files keep their formulas, and exported charts are +native Excel charts. Further capabilities include fill-in templates read from protected workbooks, live data from +REST/CSV APIs fetched by the browser or, when self-hosted, from PostgreSQL/MySQL, pivot summaries, Thai named +ranges, conditional formatting, data validation, and optional cloud save with live co-editing on a backend the +user operates. The interface is bilingual, and the suite runs 2017 automated tests. See the +[English README](README.en.md) for full documentation. --- -## ⏱️ ลองใน 60 วินาที +## ลองใน 60 วินาที

สามขั้นในทัวร์: แก้ราคาแล้วยอดขยับ, สรุป Pivot, แถบเตือนเมื่อข้อมูลต้นทางเปลี่ยน

-

สามขั้นข้างล่างนี้ อัดจากแอปจริง ไม่ได้ตัดต่อ

+

ขั้นตอนทั้งสามข้างล่าง บันทึกจากแอปจริงโดยไม่ได้ตัดต่อ

-ถ้ามีเวลาแค่นาทีเดียว — [**เปิดแอป**](https://excel-to-go.vercel.app/app) กด **ลองกับข้อมูลตัวอย่าง** บนแถบเขียว -แล้วทำสามอย่างนี้ตามลำดับ ไม่ต้องติดตั้ง ไม่ต้องสมัคร +[**เปิดแอป**](https://excel-to-go.vercel.app/app) กด **ลองกับข้อมูลตัวอย่าง** บนแถบสีเขียว แล้วทำตามสามขั้นตอนต่อไปนี้ +โดยไม่ต้องติดตั้งหรือสมัครสมาชิก -แอปเปิดมาเป็น**ตารางว่าง** เพราะงานแรกของคนที่เปิดคืองานของตัวเอง และตารางที่มีราคากาแฟของคนอื่นเต็มอยู่แล้ว -ถูกอ่านว่าเป็นข้อมูลค้าง (เคยเปิดมาพร้อมตัวอย่างเสมอ แล้วคนแรกที่ลองถามว่า "ทำไมมีข้อมูลค้าง") ข้อมูลตัวอย่าง -9 รายการจึงอยู่ห่างไปหนึ่งปุ่ม และทุกยอดรวมในนั้นเป็น**สูตร** ไม่ใช่ตัวเลข · เปิดแล้วกดย้อนกลับได้ และมีปุ่ม -**เริ่มจากตารางเปล่า** ให้กลับ +แอปเปิดมาเป็น**ตารางว่าง** เนื่องจากผู้ใช้ส่วนใหญ่เริ่มจากงานของตนเอง และตารางที่มีข้อมูลตัวอย่างอยู่แล้ว +ถูกเข้าใจว่าเป็นข้อมูลค้าง ข้อมูลตัวอย่าง 9 รายการจึงเปิดได้ด้วยปุ่มเดียว และยอดรวมทุกช่องในนั้นเป็น**สูตร** ไม่ใช่ตัวเลข +เมื่อเปิดแล้วสามารถกดย้อนกลับ หรือกด **เริ่มจากตารางเปล่า** เพื่อกลับสู่ตารางว่างได้ -| | ทำอะไร | จะเห็นอะไร | +| | ขั้นตอน | ผลที่ได้ | |---|---|---| | **1** | คลิกเซลล์ **C2** (ราคากาแฟลาเต้) แล้วพิมพ์ตัวเลขใหม่ กด Enter | คอลัมน์ **รวม** กับแถว **รวมทั้งหมด** ขยับตามทันที — ทุกยอดเป็นสูตรจริง ไม่ใช่ตัวเลขที่พิมพ์ทิ้งไว้ | | **2** | ลากเลือก **A1:E10** แล้วกด **สรุปข้อมูล (Pivot)** → จัดกลุ่มด้วย *หมวดหมู่* → ค่าที่จะสรุป *รวม* → **สร้างชีตสรุป** | ได้ชีตใหม่ที่สรุปยอดตามหมวด กลับไปแก้ราคาที่ชีตแรก แล้วดูแถบเตือน **"ข้อมูลต้นทางเปลี่ยนไปแล้ว"** โผล่บนชีตสรุป | | **3** | เลือก **A1:E10** อีกครั้ง แล้วกด **ส่งออก Excel** | เปิดไฟล์ใน Excel จริง — สูตรยังเป็นสูตร ไม่ใช่ค่าที่คำนวณทิ้งไว้ ถ้าใส่กราฟด้วยก็จะเป็น**กราฟจริงที่แก้ต่อได้** | -อยากดูที่ยากกว่านั้น: [ฝังฟอนต์ไทยใน PDF + วรรณยุกต์ซ้อน](#-ส่งออกไฟล์) · -[เขียน OOXML chart part เอง](#-กราฟจากข้อมูลในตาราง) · [ด่าน a11y ใน CI](#-การทดสอบ) · -[เอนจินคำนวณสูตรที่เขียนเอง](#-เอนจินคำนวณสูตร) +รายละเอียดทางเทคนิคที่เกี่ยวข้อง: [การฝังฟอนต์ไทยใน PDF และวรรณยุกต์ซ้อน](#ส่งออกไฟล์) · +[การเขียน OOXML chart part เอง](#กราฟจากข้อมูลในตาราง) · [ด่านตรวจการเข้าถึง (a11y) ใน CI](#การทดสอบ) · +[เอนจินคำนวณสูตร](#เอนจินคำนวณสูตร) -> อยากอ่านว่าตัดสินใจอะไรไว้บ้างและเพราะอะไร — ทุกหัวข้อในนี้เขียนเหตุผลกำกับไว้ รวมถึง -> **สิ่งที่ยังทำไม่ได้** ใน [สิ่งที่จะทำต่อ](#-สิ่งที่จะทำต่อ) +> ทุกหัวข้อในเอกสารนี้ระบุเหตุผลของการตัดสินใจไว้ด้วย รวมถึง**สิ่งที่ยังไม่รองรับ** ซึ่งรวบรวมไว้ใน +> [สิ่งที่จะทำต่อ](#สิ่งที่จะทำต่อ) --- -## 📸 หน้าตาแอป +## หน้าตาแอป

หน้าจอหลัก — ตารางกรอกข้อมูล พร้อมแถบสูตรลากวางด้านขวา

-> ภาพที่เหลืออยู่ในหัวข้อ [**ฟีเจอร์**](#-ฟีเจอร์) ข้างล่าง โดยวางไว้ข้างฟีเจอร์ที่มันอธิบาย -> ทุกภาพถ่ายจาก production build ไม่ใช่ mockup +> ภาพหน้าจออื่นอยู่ในหัวข้อ [**ฟีเจอร์**](#ฟีเจอร์) โดยวางไว้คู่กับฟีเจอร์ที่ภาพนั้นแสดง +> ทุกภาพถ่ายจาก production build ไม่ใช่ภาพจำลอง --- ---- - -### 🧪 สิ่งที่เทสต์ 2017 เคสจับไม่ได้ - -เทสต์ของผู้ช่วย AI ใช้ **mock ทั้งหมด** — มันตอบตามที่ผมคิดว่ามันจะตอบ พอเอา API key จริงมายิงคำถามธรรมดา -14 ข้อ **6 คำตอบใช้ฟังก์ชันที่เอนจินนี้ไม่มี** (`TEXTJOIN` `FIND` `RANK.EQ` `SUMPRODUCT` `CEILING` `CHAR`) -เป็นสูตร Excel ที่ถูกต้องทุกตัว แต่ขึ้น `#NAME?` ในเซลล์ หลังผู้ใช้กดปุ่มที่เขียนว่า "ใส่ลงตาราง" - -แล้ว**การแก้ครั้งแรกทำให้แย่ลง** — ผมบอกโมเดลว่า "ถ้าทำไม่ได้ ให้ตอบสูตรที่ใกล้เคียงที่สุดเท่าที่มีในรายการ" -มันจึงตอบคำถาม *"ต่อชื่อทั้งหมดเป็นบรรทัดเดียว"* ด้วย `=SUM(A2:A20)` ซึ่งได้ `0` โดยไม่มีอะไรบอกว่าผิด -**เปลี่ยน error ที่มองเห็น เป็นตัวเลขผิดที่มองไม่เห็น** - -**แล้วมันก็เกิดอีก คนละที่** ตอนที่ทุกด่านเขียวหมด — 2017 เทสต์ผ่าน, `axe` ไม่มี violation ทั้งสองหน้า -ที่สองความกว้าง — ผมลองไล่คลิก build สาธารณะแบบคนที่เพิ่งเข้ามาครั้งแรกอยู่ชั่วโมงหนึ่ง แล้วเจอสามอย่าง -ที่ไม่มีด่านไหนมองเห็น: - -- กดสูตรจาก palette ค้างไว้ครึ่งทาง **ปุ่มแถบข้างทั้ง 6 ปุ่มตายหมด** — มันยังเซ็ต mode ลง store ได้ปกติ - แต่โค้ดเรนเดอร์เป็น `pending ? … : mode` ซึ่ง `pending` กินขาด กด Escape ก็ไม่ออก คำสั่งที่กดค้างไว้ - จึงไปโผล่ทีหลัง ติดไปกับคลิกที่บังเอิญยกเลิกสูตรนั้น -- ตัวจับคำสำคัญ — ซึ่งบนเว็บสาธารณะที่ไม่ได้ตั้ง key ไม่ใช่ "ตัวสำรอง" แต่เป็น*สิ่งเดียว*ที่คนเห็น — ตอบ *"รวมยอดขายทั้งหมด"* - ด้วย `=SUM(E2)` คือรวมเซลล์เดียว และตอบ *"ต่อชื่อสินค้ากับหมวดหมู่เข้าด้วยกัน"* ด้วย `=SUM(E2)` อีกเหมือนกัน - ทั้งที่ system prompt ห้ามโมเดลทำแบบนี้มาตั้งแต่การแก้ข้างบนแล้ว — แต่ไม่เคยมีใครไปบอกตัวจับคำสำคัญ -- `axe` ผ่านตาราง 4 ชุด (สองหน้า × สองความกว้าง) ทั้งที่สำหรับ screen reader มันคือตารางธรรมดาที่ขับไม่ได้: - ไม่มี `role="grid"` ไม่มี `aria-selected` หัวตารางไม่มี `scope` ทุกเซลล์ Tab เข้าได้หมด และ - **โฟกัสของเบราว์เซอร์ค้างอยู่ที่ A1 ขณะที่ Ctrl+↓ ย้ายเคอร์เซอร์ไป A10** — ตารางเลื่อนไปเงียบ ๆ - `axe` ไม่ได้ผิด `
` is a valid table; it simply did not describe what + this application is. + +All four problems have been fixed. The conclusion is the same in each case: a passing suite confirms that the +code does what the tests specify, not that the specification is right. The full account and the fix are in +[Ask AI for a formula](#ask-ai-for-a-formula), and the check can be repeated with `npm run check:ai`. + --- -## 🔭 What's next +## What's next -What's not done yet, and why — to show this is a known gap, not something forgotten: +This section lists what has been done, what has not, and why, so that each limitation is recorded as a known +gap. While development is paused, all outstanding work is tracked in the +[open issues](https://github.com/SuruchBoss/ExcelToGo/issues). - [x] **Automated CI (GitHub Actions)** — done: lint → check:readme → test → build on every push and PR, across Node 22.12 and 24 -- [x] **Cloud save / cross-device sync** — done as **bring-your-own-backend** (see ✨ Features): +- [x] **Cloud save / cross-device sync** — done as **bring-your-own-backend** (see [Features](#features)): point it at your own Supabase project. Off by default, because this is an open-source project rather than a hosted service. Sharing a workbook with another account shipped with the live session that needed it. Still open: automatic sync (version history is done — see its own item below) -- [x] **Told up front what a file can't keep (#83)** — done (see [Told up front what a file can't keep](#-told-up-front-what-a-file-cant-keep)): pictures, shapes, Excel charts and PivotTables, macros, external links, and formulas calling a function the engine lacks · still open: hidden or protected sheets, unsupported validation and conditional-format kinds, threaded comments +- [x] **Told up front what a file can't keep (#83)** — done (see [Told up front what a file can't keep](#told-up-front-what-a-file-cant-keep)): pictures, shapes, Excel charts and PivotTables, macros, external links, and formulas calling a function the engine lacks · still open: hidden or protected sheets, unsupported validation and conditional-format kinds, threaded comments - [ ] **Open files where openpyxl wrote a picture or chart** ([#159](https://github.com/SuruchBoss/ExcelToGo/issues/159)) — today the whole file fails to open -- [x] **Sheet names Excel accepts, and exported files with real values (#54, #101)** — done (see [Multiple sheets in one file](#-multiple-sheets-in-one-file)): duplicate or invalid names refused when typed and fixed when old work opens, a failed export announced, and the export's cached values real, cross-sheet ones included +- [x] **Sheet names Excel accepts, and exported files with real values (#54, #101)** — done (see [Multiple sheets in one file](#multiple-sheets-in-one-file)): duplicate or invalid names refused when typed and fixed when old work opens, a failed export announced, and the export's cached values real, cross-sheet ones included - [ ] **The `_xlfn.` prefix for newer functions in an export** ([#156](https://github.com/SuruchBoss/ExcelToGo/issues/156)) — waiting on Microsoft's documentation, or a file real Excel saved, before any code - [ ] **The matcher filling in SUMIF/COUNTIF itself** (after #62–#64) — a question with a condition gets a button to the form today, because guessing which column holds the condition is another guess. When the value it @@ -4379,18 +4393,18 @@ What's not done yet, and why — to show this is a known gap, not something forg Also worth a policy test pinning the behaviour for a confirmed vs an unconfirmed address - [ ] **Rows drawn on past the last one, as columns are** — columns now run to the edge of the screen, but rows still stop at a new sheet's 30th, which leaves blank space under it on a tall screen -- [x] **Simultaneous editing** — done, over your own Supabase Realtime (see ✨ Features): people see +- [x] **Simultaneous editing** — done, over your own Supabase Realtime (see [Features](#features)): people see each other type, presence shows where each cursor is, the cell you have open is never overwritten mid-word, and your undo does not erase their work. **Not a CRDT** — one cell typed into twice at once leaves one value and tells the person who lost, and row inserts save and ask the others to reload. Still open: formatting, charts and comments are not synced live -- [x] **Charts/graphs** — done (see ✨ Features): bar, line and pie drawn from a range and following +- [x] **Charts/graphs** — done (see [Features](#features)): bar, line and pie drawn from a range and following the live values, placed on the grid, dragged and resized there, anchored to a cell, and carried into both the `.xlsx` and the PDF. **A chart in the `.xlsx` is now a real, editable chart** — the OOXML chart parts are written by hand and spliced into what ExcelJS produces. Still open: charts in the PDF are still pictures (jsPDF has no chart primitive), and a pie draws one series at a time -- [x] **Pivot tables** — done (see ✨ Features): group by several columns, fan one field out across +- [x] **Pivot tables** — done (see [Features](#features)): group by several columns, fan one field out across the top, summarise with sum/count/average/min/max, and get an ordinary sheet back that can be sorted, charted and exported. **A summary sheet now remembers its source** and offers a Refresh button once the numbers behind it move. Still open: refreshing is manual, one column @@ -4406,13 +4420,13 @@ What's not done yet, and why — to show this is a known gap, not something forg is already fast there (84ms to compute 20,000 × 26 from cold). Saves in the old shape still load, and the crash rescue reads both. Still open: a sheet's row count is fixed rather than growing when you type past the bottom. -- [x] **Formulas that answer with a whole table** — done (see [array formulas](#-formulas-that-answer-with-a-whole-table-array-formulas)): +- [x] **Formulas that answer with a whole table** — done (see [array formulas](#formulas-that-answer-with-a-whole-table-array-formulas)): `SEQUENCE`, `TRANSPOSE`, `UNIQUE`, `SORT` and `FILTER` spilling into the cells beside them, `#SPILL!` when they do not fit with nothing written, and operators applied across a range (`A1:A9>50` is nine answers). **A sheet with arrays on it now recomputes incrementally too** — spill regions went into the dependency graph, and a keystroke on a 3,000-row sheet holding one went from 26.4 ms to 2.3 ms. Still open: no `XMATCH`, `LET` or `LAMBDA`. -- [x] **Formulas across sheets** — done (see [formulas across sheets](#-formulas-across-sheets)): +- [x] **Formulas across sheets** — done (see [formulas across sheets](#formulas-across-sheets)): `=Sheet2!A1`, Thai names unquoted, cross-sheet staleness that follows a chain rather than one link, and cycles that span sheets. **This line used to say "`.xlsx` export writes computed values, so a round trip loses the formula", and that was not true** — the writer has been @@ -4420,18 +4434,18 @@ What's not done yet, and why — to show this is a known gap, not something forg `$A$1` all come back intact. A limitation nobody re-checks outlives the bug it described, so the claim is pinned by tests now instead of by memory: make the writer emit numbers instead of formulas and four of them fail immediately. -- [x] **The fill handle** — done (see [the fill handle](#️-the-fill-handle)): numbers, Thai days and +- [x] **The fill handle** — done (see [the fill handle](#the-fill-handle)): numbers, Thai days and months, quarters, `Item 08`, and formulas whose references move. Still open: dragging *inwards* to clear, and Excel's right-drag menu of fill options. -- [x] **Find and replace** — done (see [find and replace](#-find-and-replace)). Still open: searching +- [x] **Find and replace** — done (see [find and replace](#find-and-replace)). Still open: searching the displayed value as well as the raw text, and regular expressions. -- [x] **Freezing panes** beyond the already-sticky header row/column — done (see [the Excel keyboard](#️-the-excel-keyboard)): +- [x] **Freezing panes** beyond the already-sticky header row/column — done (see [the Excel keyboard](#the-excel-keyboard)): the split lives on the sheet, so it survives a reload, goes into undo, follows a row inserted above it, and carries into `.xlsx` both ways. Still open: no split at an arbitrary scroll position, only at the cursor, which is the shape Excel's own button has. -- [x] **Conditional formatting** — done (see ✨ Features): compare/text/rank/colour scale/data bar, +- [x] **Conditional formatting** — done (see [Features](#features)): compare/text/rank/colour scale/data bar, written into and read back from `.xlsx`. Still open: icon sets and custom-formula rules -- [x] **Cell comments** — done (see ✨ Features): a note per cell with an amber corner, following +- [x] **Cell comments** — done (see [Features](#features)): a note per cell with an amber corner, following edits to the sheet, and exported as a real Excel note. Still open: an author and timestamp, and threaded replies - [x] **`INDEX`/`MATCH`, `SUMIFS`, `COUNTIFS`, `AVERAGEIFS` and wildcards in criteria** — done @@ -4441,11 +4455,11 @@ What's not done yet, and why — to show this is a known gap, not something forg needn't be leftmost, matching exactly by default and taking its own not-found value, plus date gaps in all six units (which turned up a bug where `DAY`/`MONTH`/`YEAR` were a day out west of UTC) -- [x] **Mobile/tablet support** — done (see ✨ Features): a tab bar with a name on every tab, a menu and a +- [x] **Mobile/tablet support** — done (see [Features](#features)): a tab bar with a name on every tab, a menu and a cell tools sheet instead of rows that scrolled off screen, a second tap edits a cell, 44px targets, and a range is dragged out with a finger from a grip on the selection's corner (which scrolls the sheet to meet it) - [x] **Finding how to connect an API or database on a phone** — done: the menu's first line, the "Connect your - API" button at the top of the live-data panel, and the [in-app guide](#-connect-your-own-data-the-in-app-guide) + API" button at the top of the live-data panel, and the [in-app guide](#connect-your-own-data-the-in-app-guide) - [x] **Connecting your own API on the public site, from the browser (#110)** — done (see [From your browser](#from-your-browser-restcsv--works-on-this-site)): REST/CSV the user's machine can reach, nothing to install, the URL, header and data never through our server · the CSP opens only the origins that @@ -4461,8 +4475,8 @@ What's not done yet, and why — to show this is a known gap, not something forg - [ ] **Company cookie / single sign-on auth** — requests always go with `credentials: "omit"`; `include` would need the API to answer `Access-Control-Allow-Credentials` with no `*` origin. Not designed yet - [ ] **OAuth** — not there; only a header you type in -- [ ] **A follow-along sample** to replace the three sample sources that were removed — the list in - `src/lib/server/demoSources.ts` is kept for it +- [x] **A sample to try** in place of the three sample sources that were removed — done: + [three sample APIs](#try-a-sample-api), connected through the same form as the user's own API - [ ] **Private Network Access tested against a real internal IP** on Chrome, Edge and Safari — point three of the checklist for IT has never been tried on a real machine, which is why it says "may" - [ ] **PaynEat ERP answering CORS for this site** — the first real case of connecting from the browser; wherever @@ -4473,7 +4487,7 @@ What's not done yet, and why — to show this is a known gap, not something forg (Supabase, which the app already supports) first - [ ] **Tell "not switched on on the server" apart from "wrong token"** — the server already answers differently (403 / 401), but the token box shows one message for both; there is a link to the guide at the foot of the panel instead -- [ ] **Ctrl+B / Ctrl+I / Ctrl+U** — the buttons exist, the shortcuts don't yet +- [x] **Ctrl+B / Ctrl+I / Ctrl+U** — done: they toggle bold, italic and underline across the selection (`Cmd` on a Mac) - [ ] **Nothing compares the in-app guide with the README** — when an env var or a step changes, `/guide` has to be updated by hand - [x] **Accessibility checks in CI** — done: `npm run check:a11y` runs axe (WCAG 2.0/2.1/2.2 A+AA) on both @@ -4520,11 +4534,11 @@ What's not done yet, and why — to show this is a known gap, not something forg is a fixed list; keys, tokens, emails and Thai text come out of the stack first, and the endpoint's origin joins `connect-src` automatically. Still open: nothing reports an error that is *caught* — a failed import or a refused fetch is still only a message on screen. -- [x] **Show what a formula reads** — done (see [see what a formula is about](#-see-what-a-formula-is-about)): +- [x] **Show what a formula reads** — done (see [see what a formula is about](#see-what-a-formula-is-about)): the cells outlined on the grid and the ranges written out beside the formula bar, which is the accessible half and turns out to be the more useful one. Still open: nothing shows the other direction — which formulas read *this* cell — which is the question you ask before deleting a row. -- [x] **Works offline** — done (see [opens with the network off](#-opens-with-the-network-off)): a +- [x] **Works offline** — done (see [opens with the network off](#opens-with-the-network-off)): a hand-written service worker caches the app itself, a manifest puts it on a home screen, and the e2e gate switches the network off and reloads to prove it. Navigations are network-first so a stale document never outlives its build; `/api/*` is never cached, because a stale number @@ -4570,52 +4584,52 @@ What's not done yet, and why — to show this is a known gap, not something forg saying it. Deliberately silent, each with a test saying so: live data, which re-polls on a timer, and the pie-series picker, which is a native select that announces itself. Still open: as above, no real screen reader has heard any of it. -- [x] **A keyboard-shortcut reference in the app** — done (see [the Excel keyboard](#️-the-excel-keyboard)): +- [x] **A keyboard-shortcut reference in the app** — done (see [the Excel keyboard](#the-excel-keyboard)): `Ctrl`/`Cmd`+`/` or `F1`, or the button at the end of the sheet-tab strip. The list is kept honest by a test that reads the handlers' source, so a key cannot be added, renamed or removed without the sheet failing. Still open: it covers the grid and the global handlers, not the keys inside individual panels. -- [x] **Direct CSV import/export** — done (see ✨ Features): the delimiter is sniffed (`,`, `;`, tab), the BOM +- [x] **Direct CSV import/export** — done (see [Features](#features)): the delimiter is sniffed (`,`, `;`, tab), the BOM is stripped on the way in and written on the way out so Excel reads Thai, quoting follows RFC 4180, and the export carries computed values, **and CSV injection is neutralised** without touching negative numbers or changing this app's own round trip. Still open: non-UTF-8 files. -- [x] **Merge cells** — done (see ✨ Features): one button for merge and split, overlapping merges are +- [x] **Merge cells** — done (see [Features](#features)): one button for merge and split, overlapping merges are absorbed, it asks first only when data would be lost, and the export carries real `` elements. Still open: vertical centring, and freezing beyond the already-sticky headers. -- [x] **Template support for imported files** — done (see ✨ Features): cell locking, dropdowns and column +- [x] **Template support for imported files** — done (see [Features](#features)): cell locking, dropdowns and column widths are read from a protected file, every route into the structure is guarded, and export puts the template back together - [x] **Imported files keep their look** — done: fills, font sizes, borders, row heights and merged cells. Still open: images/charts, and authoring a template in-app -- [x] **Live data from REST API / CSV** — done (prototype, see ✨ Features), polling-based refresh +- [x] **Live data from REST API / CSV** — done (prototype, see [Features](#features)), polling-based refresh - [x] **Following paginated APIs** — done: auto-detected from a Link header / next field / cursor / a param already in the URL, and the user is told when the data came back incomplete - [x] **Rate limits explained to the user** — done: reads `Retry-After`/`X-RateLimit-Reset`, genuinely stops polling for the duration, exponential backoff for ordinary failures, shown in plain language with a live countdown - [x] **Recalculate only what changed, and stop rendering rows nobody can see** — done (see - [the formula engine](#-formula-engine)): an AST cache plus a dependency graph read off the + [the formula engine](#formula-engine)): an AST cache plus a dependency graph read off the syntax tree takes one edit in a 3,000-row sheet from **1,244.9 ms to 3.6 ms**, and a 5,000-row sheet keeps 41 `
` ที่มี `
` *เป็น* ตารางที่ถูกต้องจริง มันแค่ไม่ใช่สิ่งที่แอปนี้เป็น - -ทั้งสี่ข้อแก้แล้ว บทเรียนเหมือนเดิมทุกครั้ง: เทสต์เขียวบอกว่าโค้ดทำตามที่เทสต์เขียนไว้ และไม่ได้บอกอะไรเลย -ว่าสิ่งที่เขียนไว้นั้นถูกหรือเปล่า - -→ [เรื่องเต็มและวิธีแก้](#-ถาม-ai-หาสูตร) · รันเองได้ด้วย `npm run check:ai` - -## 💼 ปัญหาที่แก้ได้ +## ปัญหาที่แก้ได้ สรุปสำหรับคนที่ต้องตัดสินใจว่าจะใช้หรือไม่ — เล่าจาก**ปัญหาที่ทีมจ่ายอยู่ทุกเดือน** ไม่ใช่จากรายการฟีเจอร์ แต่ละข้อแก้ด้วยหลายฟีเจอร์ที่ทำงานต่อกัน และชื่อฟีเจอร์แต่ละตัวลิงก์ไปที่รายละเอียดของมันข้างล่าง · เนื้อหาชุดเดียวกับส่วนแรกของ @@ -154,9 +136,9 @@ top/bottom ranks, colour scales and data bars — and round-trips through Excel' **แก้ด้วย** -1. **[ถาม AI เป็นภาษาไทย](#-ถาม-ai-หาสูตร)** — พิมพ์ว่า “รวมยอดขายเฉพาะสาขาเหนือ” แล้วได้สูตรพร้อมคำอธิบายก่อนตัดสินใจใส่ — ต้องใช้ [API key ของคุณเอง](#-ใส่-api-key-ของคุณเอง-byok) ถ้าไม่มี key ระบบจะเดาจากคำสำคัญ ซึ่งได้แค่สูตรพื้นฐาน -2. **[สูตรพร้อมใช้ 37 แบบ](#-ใส่สูตรแบบลากวาง)** — ไม่อยากพิมพ์ก็เลือกจากรายการ กรอกทีละช่อง แล้วกดปุ่มเป้าเล็งลากเลือกช่วงจากตารางจริง แทนการพิมพ์ที่อยู่เซลล์เอง -3. **[ตั้งชื่อช่วงเป็นภาษาไทย](#-ตั้งชื่อช่วงข้อมูล-named-ranges)** — `=SUM(ยอดขาย)` แทน `=SUM(B2:B500)` — คนที่มารับไฟล์ต่ออ่านสูตรรู้เรื่อง โดยไม่ต้องตามไปถามคนเขียน +1. **[ถาม AI เป็นภาษาไทย](#ถาม-ai-หาสูตร)** — พิมพ์ว่า “รวมยอดขายเฉพาะสาขาเหนือ” แล้วได้สูตรพร้อมคำอธิบายก่อนตัดสินใจใส่ — ต้องใช้ [API key ของคุณเอง](#ใส่-api-key-ของคุณเอง-byok) ถ้าไม่มี key ระบบจะเดาจากคำสำคัญ ซึ่งได้แค่สูตรพื้นฐาน +2. **[สูตรพร้อมใช้ 37 แบบ](#ใส่สูตรแบบลากวาง)** — ไม่อยากพิมพ์ก็เลือกจากรายการ กรอกทีละช่อง แล้วกดปุ่มเป้าเล็งลากเลือกช่วงจากตารางจริง แทนการพิมพ์ที่อยู่เซลล์เอง +3. **[ตั้งชื่อช่วงเป็นภาษาไทย](#ตั้งชื่อช่วงข้อมูล-named-ranges)** — `=SUM(ยอดขาย)` แทน `=SUM(B2:B500)` — คนที่มารับไฟล์ต่ออ่านสูตรรู้เรื่อง โดยไม่ต้องตามไปถามคนเขียน > **ผลลัพธ์:** คนในทีมทำรายงานเองได้ภายในวันนั้น และไฟล์ยังอ่านรู้เรื่องหลังจากคนเขียนย้ายไปแล้ว @@ -169,10 +151,10 @@ top/bottom ranks, colour scales and data bars — and round-trips through Excel' **แก้ด้วย** -1. **[จำกัดสิ่งที่กรอกได้](#-จำกัดสิ่งที่กรอกได้-data-validation)** — ช่องกลายเป็น dropdown หรือรับเฉพาะตัวเลขในช่วงที่กำหนด กรอกนอกกติกาแล้ว**ไม่ถูกบันทึก** — ไม่ใช่บันทึกไว้แล้วค่อยเตือน -2. **[เห็นว่าสูตรอ่านช่องไหน](#-ดูว่าสูตรนี้พูดถึงช่องไหน)** — คลิกที่ยอดรวม แล้วช่องที่มันอ่านจะสว่างขึ้นบนตาราง ช่วงที่เลยไปหนึ่งคอลัมน์เห็นได้ด้วยตา -3. **[เลือกช่วงด้วยการลาก](#️-ลากมุมเพื่อเติม-fill-handle)** — ลากจากตารางแทนพิมพ์ที่อยู่ และลากมุมเพื่อเติมสูตรลงทั้งคอลัมน์ การอ้างอิงขยับตามถูก ส่วน `$A$1` ไม่ขยับ -4. **[จัดรูปแบบตามเงื่อนไข](#-จัดรูปแบบตามเงื่อนไข-conditional-formatting)** — ค่าที่ผิดปกติถูกระบายสีเอง ไม่ต้องไล่ดูทีละแถว +1. **[จำกัดสิ่งที่กรอกได้](#จำกัดสิ่งที่กรอกได้-data-validation)** — ช่องกลายเป็น dropdown หรือรับเฉพาะตัวเลขในช่วงที่กำหนด กรอกนอกกติกาแล้ว**ไม่ถูกบันทึก** — ไม่ใช่บันทึกไว้แล้วค่อยเตือน +2. **[เห็นว่าสูตรอ่านช่องไหน](#ดูว่าสูตรนี้พูดถึงช่องไหน)** — คลิกที่ยอดรวม แล้วช่องที่มันอ่านจะสว่างขึ้นบนตาราง ช่วงที่เลยไปหนึ่งคอลัมน์เห็นได้ด้วยตา +3. **[เลือกช่วงด้วยการลาก](#ลากมุมเพื่อเติม-fill-handle)** — ลากจากตารางแทนพิมพ์ที่อยู่ และลากมุมเพื่อเติมสูตรลงทั้งคอลัมน์ การอ้างอิงขยับตามถูก ส่วน `$A$1` ไม่ขยับ +4. **[จัดรูปแบบตามเงื่อนไข](#จัดรูปแบบตามเงื่อนไข-conditional-formatting)** — ค่าที่ผิดปกติถูกระบายสีเอง ไม่ต้องไล่ดูทีละแถว > **ผลลัพธ์:** ข้อมูลผิดถูกกันไว้ตั้งแต่ตอนกรอก ไม่ใช่ถูกเจอตอนปิดงบ @@ -185,9 +167,9 @@ top/bottom ranks, colour scales and data bars — and round-trips through Excel' **แก้ด้วย** -1. **[ต่อ API จากเบราว์เซอร์](#ต่อจากเบราว์เซอร์-restcsv--ใช้บนเว็บนี้ได้เลย)** — ใส่ URL กับ header ครั้งเดียวบนเว็บนี้ได้เลย API หลัง VPN ก็ได้ถ้าเครื่องคุณเปิดถึง · ถ้าต้องการ [PostgreSQL / MySQL ตรง](#-ต่อฐานข้อมูลตรง-ๆ-postgresql--mysql) ให้รันแอปบนเซิร์ฟเวอร์ของคุณเอง คำสั่งรันแบบอ่านอย่างเดียว ฐานข้อมูลเป็นคนปฏิเสธการเขียนเอง -2. **[กด “ใส่ลงตาราง”](#-ข้อมูลสดจาก-api--csv-prototype)** — ผู้ใช้เลือกช่อง แล้วเลือกได้ว่าจะเอาทั้งตารางหรือตัวเลขสรุปค่าเดียว ค่าอัปเดตเองตามรอบ และเข้าสูตรต่อได้เหมือนเซลล์ธรรมดา -3. **[Pivot และกราฟที่ตามข้อมูล](#-สรุปข้อมูล-pivot)** — สรุปข้อมูลที่ผูกกับต้นทาง กดรีเฟรชแล้วได้ของใหม่ และ[กราฟ](#-กราฟจากข้อมูลในตาราง)ขยับตามตัวเลขทันที +1. **[ต่อ API จากเบราว์เซอร์](#ต่อจากเบราว์เซอร์-restcsv--ใช้บนเว็บนี้ได้เลย)** — ใส่ URL กับ header ครั้งเดียวบนเว็บนี้ได้เลย API หลัง VPN ก็ได้ถ้าเครื่องคุณเปิดถึง · ถ้าต้องการ [PostgreSQL / MySQL ตรง](#ต่อฐานข้อมูลตรง-ๆ-postgresql--mysql) ให้รันแอปบนเซิร์ฟเวอร์ของคุณเอง คำสั่งรันแบบอ่านอย่างเดียว ฐานข้อมูลเป็นคนปฏิเสธการเขียนเอง +2. **[กด “ใส่ลงตาราง”](#ข้อมูลสดจาก-api--csv-prototype)** — ผู้ใช้เลือกช่อง แล้วเลือกได้ว่าจะเอาทั้งตารางหรือตัวเลขสรุปค่าเดียว ค่าอัปเดตเองตามรอบ และเข้าสูตรต่อได้เหมือนเซลล์ธรรมดา +3. **[Pivot และกราฟที่ตามข้อมูล](#สรุปข้อมูล-pivot)** — สรุปข้อมูลที่ผูกกับต้นทาง กดรีเฟรชแล้วได้ของใหม่ และ[กราฟ](#กราฟจากข้อมูลในตาราง)ขยับตามตัวเลขทันที > **ผลลัพธ์:** งานประจำเดือนหายไปหนึ่งอย่าง และเลขในไฟล์เป็นเลขตอนนี้ @@ -200,10 +182,10 @@ top/bottom ranks, colour scales and data bars — and round-trips through Excel' **แก้ด้วย** -1. **[เปิด .xlsx แล้วหน้าตาเหมือนเดิม](#-เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม)** — สี เส้นขอบ ขนาดตัวอักษร ความสูงแถว และเซลล์ที่ merge ไว้มาครบ -2. **[แม่แบบจากไฟล์ที่ล็อกไว้](#-แม่แบบ-template-จากไฟล์-excel)** — ไฟล์ที่ protect ไว้ถูกอ่านเป็นแม่แบบที่รู้เองว่าช่องไหนให้กรอก ช่องไหนห้ามแตะ พร้อม dropdown จากไฟล์ -3. **[ส่งออกแล้วยังทำงานต่อได้](#-ส่งออกไฟล์)** — สูตรยังเป็นสูตร กราฟเป็นกราฟ Excel จริง และ [CSV](#-นำเข้าส่งออก-csv) ถูกกันสูตรอันตรายที่แฝงมากับข้อมูล (CSV injection) -4. **[PDF ภาษาไทยถูกต้อง](#-ส่งออกไฟล์)** — ฝังฟอนต์ไทยไปด้วย และวางวรรณยุกต์ซ้อนสระบนถูกตำแหน่ง +1. **[เปิด .xlsx แล้วหน้าตาเหมือนเดิม](#เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม)** — สี เส้นขอบ ขนาดตัวอักษร ความสูงแถว และเซลล์ที่ merge ไว้มาครบ +2. **[แม่แบบจากไฟล์ที่ล็อกไว้](#แม่แบบ-template-จากไฟล์-excel)** — ไฟล์ที่ protect ไว้ถูกอ่านเป็นแม่แบบที่รู้เองว่าช่องไหนให้กรอก ช่องไหนห้ามแตะ พร้อม dropdown จากไฟล์ +3. **[ส่งออกแล้วยังทำงานต่อได้](#ส่งออกไฟล์)** — สูตรยังเป็นสูตร กราฟเป็นกราฟ Excel จริง และ [CSV](#นำเข้าส่งออก-csv) ถูกกันสูตรอันตรายที่แฝงมากับข้อมูล (CSV injection) +4. **[PDF ภาษาไทยถูกต้อง](#ส่งออกไฟล์)** — ฝังฟอนต์ไทยไปด้วย และวางวรรณยุกต์ซ้อนสระบนถูกตำแหน่ง > **ผลลัพธ์:** ไม่ต้องทิ้งไฟล์เดิม และไฟล์ที่ส่งออกไป คนรับเปิดใน Excel แล้วทำงานต่อได้ @@ -216,10 +198,10 @@ top/bottom ranks, colour scales and data bars — and round-trips through Excel' **แก้ด้วย** -1. **[ทำงานในเบราว์เซอร์ล้วน](#-ความปลอดภัย--ทดสอบอะไรไปบ้าง)** — ไม่มีบัญชี ไม่มีการอัปโหลด ไฟล์ไม่ออกจากเครื่อง และนโยบายความปลอดภัยของหน้าเว็บ (CSP) ปิดทางไม่ให้ส่งข้อมูลออกไปที่อื่น — มี e2e ยิงทดสอบจริง · ยกเว้นตอนกดถาม AI ที่คำถามกับหัวคอลัมน์ถูกส่งไปที่ Anthropic (มี key) หรือเซิร์ฟเวอร์ของเว็บนี้ (ไม่มี key) -2. **[AI ด้วย key ของคุณเอง](#-ใส่-api-key-ของคุณเอง-byok)** — คำถามถูกส่งจากเบราว์เซอร์ตรงไปที่ Anthropic ด้วย key ของคุณ ไม่ผ่านเซิร์ฟเวอร์ของเว็บนี้ และส่งเฉพาะคำถาม ช่วงเซลล์ และหัวคอลัมน์ ไม่ส่งไฟล์ -3. **[นับการใช้งานโดยไม่ระบุตัวตน](#-นับยอดใช้งานโดยพิสูจน์ได้ว่าระบุตัวใครไม่ได้)** — นับได้แค่ว่าวันนี้มีการเปิดแอปกี่ครั้ง ไม่เก็บ IP ไม่มีคุกกี้ และเคารพ Do Not Track · รายงานข้อผิดพลาด (ถ้าเปิดใช้) ลบข้อความภาษาไทยและ key ออกก่อนส่ง -4. **[คลาวด์บน backend ของคุณเอง](#️-บันทึกบนคลาวด์-ต่อ-backend-ของคุณเอง)** — อยากบันทึกออนไลน์หรือแก้พร้อมกัน ก็ต่อ Supabase ของคุณเอง และสิทธิ์การเข้าถึงระดับแถวมีเทสต์คุมไว้ +1. **[ทำงานในเบราว์เซอร์ล้วน](#ความปลอดภัย--ทดสอบอะไรไปบ้าง)** — ไม่มีบัญชี ไม่มีการอัปโหลด ไฟล์ไม่ออกจากเครื่อง และนโยบายความปลอดภัยของหน้าเว็บ (CSP) ปิดทางไม่ให้ส่งข้อมูลออกไปที่อื่น — มี e2e ยิงทดสอบจริง · ยกเว้นตอนกดถาม AI ที่คำถามกับหัวคอลัมน์ถูกส่งไปที่ Anthropic (มี key) หรือเซิร์ฟเวอร์ของเว็บนี้ (ไม่มี key) +2. **[AI ด้วย key ของคุณเอง](#ใส่-api-key-ของคุณเอง-byok)** — คำถามถูกส่งจากเบราว์เซอร์ตรงไปที่ Anthropic ด้วย key ของคุณ ไม่ผ่านเซิร์ฟเวอร์ของเว็บนี้ และส่งเฉพาะคำถาม ช่วงเซลล์ และหัวคอลัมน์ ไม่ส่งไฟล์ +3. **[นับการใช้งานโดยไม่ระบุตัวตน](#นับยอดใช้งานโดยพิสูจน์ได้ว่าระบุตัวใครไม่ได้)** — นับได้แค่ว่าวันนี้มีการเปิดแอปกี่ครั้ง ไม่เก็บ IP ไม่มีคุกกี้ และเคารพ Do Not Track · รายงานข้อผิดพลาด (ถ้าเปิดใช้) ลบข้อความภาษาไทยและ key ออกก่อนส่ง +4. **[คลาวด์บน backend ของคุณเอง](#บันทึกบนคลาวด์-ต่อ-backend-ของคุณเอง)** — อยากบันทึกออนไลน์หรือแก้พร้อมกัน ก็ต่อ Supabase ของคุณเอง และสิทธิ์การเข้าถึงระดับแถวมีเทสต์คุมไว้ > **ผลลัพธ์:** เริ่มใช้ได้โดยไม่ต้องรอใครอนุมัติ และตอบคำถาม “ข้อมูลไปไหน” ได้ด้วยโค้ดที่เปิดให้อ่าน ไม่ใช่ด้วยหน้านโยบาย @@ -232,10 +214,10 @@ top/bottom ranks, colour scales and data bars — and round-trips through Excel' **แก้ด้วย** -1. **[บันทึกอัตโนมัติ + ย้อนกลับได้](#-บันทึกอัตโนมัติ--undoredo)** — ทุกการแก้ถูกเก็บในเบราว์เซอร์ทันที และกด Ctrl+Z ย้อนได้ทีละขั้น -2. **[บันทึกไม่ลงก็บอกตรง ๆ](#-บันทึกอัตโนมัติ--undoredo)** — ถ้าเบราว์เซอร์ปฏิเสธการบันทึก เช่นพื้นที่เต็ม แอปขึ้นแถบเตือนทันทีและยังส่งออกไฟล์ได้ — ไม่ใช่เงียบไว้แล้วงานหายตอนปิดแท็บ -3. **[พังก็ยังเอางานออกได้](#-แอปพังแล้วยังเอาไฟล์ออกไปได้)** — ถ้าแอปพัง หน้าที่ขึ้นมาให้ดาวน์โหลดงานเป็นไฟล์ได้ก่อนอย่างอื่น และแอป[เปิดได้แม้ไม่มีเน็ต](#-เปิดได้ตอนไม่มีเน็ต) -4. **[แก้พร้อมกัน + ประวัติเวอร์ชัน](#-แก้พร้อมกันหลายคน)** — หลายคนแก้ไฟล์เดียวกันได้สด ๆ ปุ่มย้อนกลับของเราไม่ลบงานของเขา และย้อนไปดูฉบับก่อนหน้าได้ (ต่อ Supabase ของคุณเอง) +1. **[บันทึกอัตโนมัติ + ย้อนกลับได้](#บันทึกอัตโนมัติ--undoredo)** — ทุกการแก้ถูกเก็บในเบราว์เซอร์ทันที และกด Ctrl+Z ย้อนได้ทีละขั้น +2. **[บันทึกไม่ลงก็บอกตรง ๆ](#บันทึกอัตโนมัติ--undoredo)** — ถ้าเบราว์เซอร์ปฏิเสธการบันทึก เช่นพื้นที่เต็ม แอปขึ้นแถบเตือนทันทีและยังส่งออกไฟล์ได้ — ไม่ใช่เงียบไว้แล้วงานหายตอนปิดแท็บ +3. **[พังก็ยังเอางานออกได้](#แอปพังแล้วยังเอาไฟล์ออกไปได้)** — ถ้าแอปพัง หน้าที่ขึ้นมาให้ดาวน์โหลดงานเป็นไฟล์ได้ก่อนอย่างอื่น และแอป[เปิดได้แม้ไม่มีเน็ต](#เปิดได้ตอนไม่มีเน็ต) +4. **[แก้พร้อมกัน + ประวัติเวอร์ชัน](#แก้พร้อมกันหลายคน)** — หลายคนแก้ไฟล์เดียวกันได้สด ๆ ปุ่มย้อนกลับของเราไม่ลบงานของเขา และย้อนไปดูฉบับก่อนหน้าได้ (ต่อ Supabase ของคุณเอง) > **ผลลัพธ์:** งานที่พิมพ์ไปแล้วไม่หายแบบเงียบ ๆ และมีไฟล์ฉบับเดียวที่ทุกคนแก้ร่วมกัน @@ -244,102 +226,105 @@ top/bottom ranks, colour scales and data bars — and round-trips through Excel' --- -## 📋 สารบัญ - -- [ลองใน 60 วินาที](#️-ลองใน-60-วินาที) -- [หน้าตาแอป](#-หน้าตาแอป) -- [ปัญหาที่แก้ได้](#-ปัญหาที่แก้ได้) -- [ทำไมถึงทำโปรเจกต์นี้](#-ทำไมถึงทำโปรเจกต์นี้) -- [วิธีรัน](#-วิธีรัน) -- [ฟีเจอร์](#-ฟีเจอร์) - - [ถาม AI หาสูตร](#-ถาม-ai-หาสูตร) - - [ใส่ API key ของคุณเอง (BYOK)](#-ใส่-api-key-ของคุณเอง-byok) - - [ข้อมูลสดจาก API / CSV (prototype)](#-ข้อมูลสดจาก-api--csv-prototype) - - [ต่อฐานข้อมูลตรง ๆ (PostgreSQL / MySQL)](#-ต่อฐานข้อมูลตรง-ๆ-postgresql--mysql) - - [คู่มือต่อข้อมูลของคุณเอง (ในเว็บ)](#-คู่มือต่อข้อมูลของคุณเอง-ในเว็บ) - - [ตารางกรอกข้อมูล (Spreadsheet Grid)](#-ตารางกรอกข้อมูล-spreadsheet-grid) - - [บันทึกอัตโนมัติ + Undo/Redo](#-บันทึกอัตโนมัติ--undoredo) - - [แอปพังแล้วยังเอาไฟล์ออกไปได้](#-แอปพังแล้วยังเอาไฟล์ออกไปได้) - - [คัดลอก / ตัด / วาง](#️-คัดลอก--ตัด--วาง) - - [จัดรูปแบบเซลล์](#-จัดรูปแบบเซลล์) - - [สีพื้นเซลล์](#-สีพื้นเซลล์) - - [เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์](#-เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์) - - [วันที่และเวลา](#-วันที่และเวลา) - - [วันที่แบบ พ.ศ.](#-วันที่แบบ-พศ) - - [กราฟจากข้อมูลในตาราง](#-กราฟจากข้อมูลในตาราง) - - [จัดรูปแบบตามเงื่อนไข (Conditional Formatting)](#-จัดรูปแบบตามเงื่อนไข-conditional-formatting) - - [คอมเมนต์ในเซลล์](#-คอมเมนต์ในเซลล์) - - [จำกัดสิ่งที่กรอกได้ (Data Validation)](#-จำกัดสิ่งที่กรอกได้-data-validation) - - [ตั้งชื่อช่วงข้อมูล (Named Ranges)](#-ตั้งชื่อช่วงข้อมูล-named-ranges) - - [บันทึกบนคลาวด์ (ต่อ backend ของคุณเอง)](#️-บันทึกบนคลาวด์-ต่อ-backend-ของคุณเอง) - - [แก้พร้อมกันหลายคน](#-แก้พร้อมกันหลายคน) - - [คีย์บอร์ดแบบเดียวกับ Excel](#️-คีย์บอร์ดแบบเดียวกับ-excel) - - [สูตรที่ตอบมาเป็นทั้งตาราง (array formula)](#-สูตรที่ตอบมาเป็นทั้งตาราง-array-formula) - - [สูตรข้ามชีต](#-สูตรข้ามชีต) - - [ดูว่าสูตรนี้พูดถึงช่องไหน](#-ดูว่าสูตรนี้พูดถึงช่องไหน) - - [ลากมุมเพื่อเติม (fill handle)](#️-ลากมุมเพื่อเติม-fill-handle) - - [ค้นหาและแทนที่](#-ค้นหาและแทนที่) - - [เปิดได้ตอนไม่มีเน็ต](#-เปิดได้ตอนไม่มีเน็ต) - - [ใช้บนมือถือได้](#-ใช้บนมือถือได้) - - [คัดลอก วาง เติมลงด้วยนิ้ว](#-คัดลอก-วาง-เติมลงด้วยนิ้ว) - - [ลากเลือกช่วงด้วยนิ้วโดยข้อมูลไม่หลุดจอ](#-ลากเลือกช่วงด้วยนิ้วโดยข้อมูลไม่หลุดจอ) - - [แตะเซลล์ใส่ลงสูตรบนมือถือ](#-แตะเซลล์ใส่ลงสูตรบนมือถือ) - - [แท็บเล็ตและมือถือจอพับ](#-แท็บเล็ตและมือถือจอพับ) - - [ข้อความทีละอัน และจอเตี้ย](#-ข้อความทีละอัน-และจอเตี้ย) - - [แทรก/ลบแถว-คอลัมน์](#-แทรกลบแถว-คอลัมน์) - - [คลิกขวาที่เซลล์](#️-คลิกขวาที่เซลล์) - - [ลากปรับความกว้างคอลัมน์](#-ลากปรับความกว้างคอลัมน์) - - [รวมเซลล์ (merge)](#-รวมเซลล์-merge) - - [เรียงลำดับและกรองข้อมูล](#-เรียงลำดับและกรองข้อมูล) - - [สรุปข้อมูล (Pivot)](#-สรุปข้อมูล-pivot) - - [หลายชีตในไฟล์เดียว](#-หลายชีตในไฟล์เดียว) - - [นำเข้าไฟล์ Excel ที่มีอยู่แล้ว](#-นำเข้าไฟล์-excel-ที่มีอยู่แล้ว) - - [บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้](#-บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้) - - [เปิดไฟล์ใหม่โดยไม่ทับงานเดิม](#-เปิดไฟล์ใหม่โดยไม่ทับงานเดิม) - - [เริ่มจากตารางว่าง · ไฟล์ใหม่](#-เริ่มจากตารางว่าง--ไฟล์ใหม่) - - [ใส่สูตรแบบลากวาง](#-ใส่สูตรแบบลากวาง) - - [ส่งออกไฟล์](#-ส่งออกไฟล์) - - [นำเข้า/ส่งออก CSV](#-นำเข้าส่งออก-csv) - - [เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม](#-เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม) - - [แม่แบบ (Template) จากไฟล์ Excel](#-แม่แบบ-template-จากไฟล์-excel) - - [นับยอดใช้งานโดยพิสูจน์ได้ว่าระบุตัวใครไม่ได้](#-นับยอดใช้งานโดยพิสูจน์ได้ว่าระบุตัวใครไม่ได้) - - [หน้า landing อธิบายแอป](#-หน้า-landing-อธิบายแอป) - - [คำถามที่พบบ่อยบนหน้าแรก](#-คำถามที่พบบ่อยบนหน้าแรก) - - [หน้าสอนสูตรทีละตัว](#-หน้าสอนสูตรทีละตัว) - - [สองภาษา (ไทย / English)](#-สองภาษา-ไทย--english) -- [เทคโนโลยีที่ใช้](#-เทคโนโลยีที่ใช้) -- [สถาปัตยกรรม](#-สถาปัตยกรรม) -- [โครงสร้างโปรเจกต์](#-โครงสร้างโปรเจกต์) -- [เอนจินคำนวณสูตร](#-เอนจินคำนวณสูตร) -- [ระบบสองภาษา (i18n)](#-ระบบสองภาษา-i18n) -- [ความปลอดภัย — ทดสอบอะไรไปบ้าง](#-ความปลอดภัย--ทดสอบอะไรไปบ้าง) -- [Lighthouse](#-lighthouse) -- [การทดสอบ](#-การทดสอบ) -- [สิ่งที่จะทำต่อ](#-สิ่งที่จะทำต่อ) +## สารบัญ + +- [ภาพรวม](#ภาพรวม) +- [ความสามารถหลัก](#ความสามารถหลัก) +- [การจัดการข้อมูล](#การจัดการข้อมูล) +- [ลองใน 60 วินาที](#ลองใน-60-วินาที) +- [หน้าตาแอป](#หน้าตาแอป) +- [ปัญหาที่แก้ได้](#ปัญหาที่แก้ได้) +- [วัตถุประสงค์ของโปรเจกต์](#วัตถุประสงค์ของโปรเจกต์) +- [การติดตั้งและใช้งาน](#การติดตั้งและใช้งาน) +- [ฟีเจอร์](#ฟีเจอร์) + - [ถาม AI หาสูตร](#ถาม-ai-หาสูตร) + - [ใส่ API key ของคุณเอง (BYOK)](#ใส่-api-key-ของคุณเอง-byok) + - [ข้อมูลสดจาก API / CSV (prototype)](#ข้อมูลสดจาก-api--csv-prototype) + - [ต่อฐานข้อมูลตรง ๆ (PostgreSQL / MySQL)](#ต่อฐานข้อมูลตรง-ๆ-postgresql--mysql) + - [คู่มือต่อข้อมูลของคุณเอง (ในเว็บ)](#คู่มือต่อข้อมูลของคุณเอง-ในเว็บ) + - [ตารางกรอกข้อมูล (Spreadsheet Grid)](#ตารางกรอกข้อมูล-spreadsheet-grid) + - [บันทึกอัตโนมัติ + Undo/Redo](#บันทึกอัตโนมัติ--undoredo) + - [แอปพังแล้วยังเอาไฟล์ออกไปได้](#แอปพังแล้วยังเอาไฟล์ออกไปได้) + - [คัดลอก / ตัด / วาง](#คัดลอก--ตัด--วาง) + - [จัดรูปแบบเซลล์](#จัดรูปแบบเซลล์) + - [สีพื้นเซลล์](#สีพื้นเซลล์) + - [เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์](#เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์) + - [วันที่และเวลา](#วันที่และเวลา) + - [วันที่แบบ พ.ศ.](#วันที่แบบ-พศ) + - [กราฟจากข้อมูลในตาราง](#กราฟจากข้อมูลในตาราง) + - [จัดรูปแบบตามเงื่อนไข (Conditional Formatting)](#จัดรูปแบบตามเงื่อนไข-conditional-formatting) + - [คอมเมนต์ในเซลล์](#คอมเมนต์ในเซลล์) + - [จำกัดสิ่งที่กรอกได้ (Data Validation)](#จำกัดสิ่งที่กรอกได้-data-validation) + - [ตั้งชื่อช่วงข้อมูล (Named Ranges)](#ตั้งชื่อช่วงข้อมูล-named-ranges) + - [บันทึกบนคลาวด์ (ต่อ backend ของคุณเอง)](#บันทึกบนคลาวด์-ต่อ-backend-ของคุณเอง) + - [แก้พร้อมกันหลายคน](#แก้พร้อมกันหลายคน) + - [คีย์บอร์ดแบบเดียวกับ Excel](#คีย์บอร์ดแบบเดียวกับ-excel) + - [สูตรที่ตอบมาเป็นทั้งตาราง (array formula)](#สูตรที่ตอบมาเป็นทั้งตาราง-array-formula) + - [สูตรข้ามชีต](#สูตรข้ามชีต) + - [ดูว่าสูตรนี้พูดถึงช่องไหน](#ดูว่าสูตรนี้พูดถึงช่องไหน) + - [ลากมุมเพื่อเติม (fill handle)](#ลากมุมเพื่อเติม-fill-handle) + - [ค้นหาและแทนที่](#ค้นหาและแทนที่) + - [เปิดได้ตอนไม่มีเน็ต](#เปิดได้ตอนไม่มีเน็ต) + - [ใช้บนมือถือได้](#ใช้บนมือถือได้) + - [คัดลอก วาง เติมลงด้วยนิ้ว](#คัดลอก-วาง-เติมลงด้วยนิ้ว) + - [ลากเลือกช่วงด้วยนิ้วโดยข้อมูลไม่หลุดจอ](#ลากเลือกช่วงด้วยนิ้วโดยข้อมูลไม่หลุดจอ) + - [แตะเซลล์ใส่ลงสูตรบนมือถือ](#แตะเซลล์ใส่ลงสูตรบนมือถือ) + - [แท็บเล็ตและมือถือจอพับ](#แท็บเล็ตและมือถือจอพับ) + - [ข้อความทีละอัน และจอเตี้ย](#ข้อความทีละอัน-และจอเตี้ย) + - [แทรก/ลบแถว-คอลัมน์](#แทรกลบแถว-คอลัมน์) + - [คลิกขวาที่เซลล์](#คลิกขวาที่เซลล์) + - [ลากปรับความกว้างคอลัมน์](#ลากปรับความกว้างคอลัมน์) + - [รวมเซลล์ (merge)](#รวมเซลล์-merge) + - [เรียงลำดับและกรองข้อมูล](#เรียงลำดับและกรองข้อมูล) + - [สรุปข้อมูล (Pivot)](#สรุปข้อมูล-pivot) + - [หลายชีตในไฟล์เดียว](#หลายชีตในไฟล์เดียว) + - [นำเข้าไฟล์ Excel ที่มีอยู่แล้ว](#นำเข้าไฟล์-excel-ที่มีอยู่แล้ว) + - [บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้](#บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้) + - [เปิดไฟล์ใหม่โดยไม่ทับงานเดิม](#เปิดไฟล์ใหม่โดยไม่ทับงานเดิม) + - [เริ่มจากตารางว่าง · ไฟล์ใหม่](#เริ่มจากตารางว่าง--ไฟล์ใหม่) + - [ใส่สูตรแบบลากวาง](#ใส่สูตรแบบลากวาง) + - [ส่งออกไฟล์](#ส่งออกไฟล์) + - [นำเข้า/ส่งออก CSV](#นำเข้าส่งออก-csv) + - [เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม](#เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม) + - [แม่แบบ (Template) จากไฟล์ Excel](#แม่แบบ-template-จากไฟล์-excel) + - [นับยอดใช้งานโดยพิสูจน์ได้ว่าระบุตัวใครไม่ได้](#นับยอดใช้งานโดยพิสูจน์ได้ว่าระบุตัวใครไม่ได้) + - [หน้า landing อธิบายแอป](#หน้า-landing-อธิบายแอป) + - [คำถามที่พบบ่อยบนหน้าแรก](#คำถามที่พบบ่อยบนหน้าแรก) + - [หน้าสอนสูตรทีละตัว](#หน้าสอนสูตรทีละตัว) + - [สองภาษา (ไทย / English)](#สองภาษา-ไทย--english) +- [เทคโนโลยีที่ใช้](#เทคโนโลยีที่ใช้) +- [สถาปัตยกรรม](#สถาปัตยกรรม) +- [โครงสร้างโปรเจกต์](#โครงสร้างโปรเจกต์) +- [เอนจินคำนวณสูตร](#เอนจินคำนวณสูตร) +- [ระบบสองภาษา (i18n)](#ระบบสองภาษา-i18n) +- [ความปลอดภัย — ทดสอบอะไรไปบ้าง](#ความปลอดภัย--ทดสอบอะไรไปบ้าง) +- [Lighthouse](#lighthouse) +- [การทดสอบ](#การทดสอบ) +- [สิ่งที่จะทำต่อ](#สิ่งที่จะทำต่อ) --- -## 🎯 ทำไมถึงทำโปรเจกต์นี้ +## วัตถุประสงค์ของโปรเจกต์ -จุดตั้งต้นคือ pain point จริงตอนใช้ Excel: **บนคอม/ออนไลน์กรอกยาก, UI ไม่ friendly, จำสูตรไม่ได้, ลากจอไปมา -แล้วงงว่าอยู่แถวไหนคอลัมน์ไหน** โปรเจกต์นี้เลยตั้งใจแก้ปัญหานั้นตรงๆ แทนที่จะทำ "โปรแกรมคำนวณ" ทั่วไป -จึงมีรายละเอียดที่ต้องคิดมากกว่าที่เห็น: +โปรเจกต์นี้เริ่มจากปัญหาที่พบจริงในการใช้ Excel ได้แก่ **การกรอกข้อมูลบนคอมพิวเตอร์และบนเว็บทำได้ยาก +หน้าจอไม่เป็นมิตร จำสูตรไม่ได้ และหลงตำแหน่งแถวหรือคอลัมน์เมื่อเลื่อนตารางขนาดใหญ่** เป้าหมายคือแก้ปัญหาเหล่านี้โดยตรง +แทนการสร้างเครื่องคิดเลขทั่วไป ซึ่งมีประเด็นทางเทคนิคที่ต้องออกแบบอย่างรอบคอบดังนี้ -- ผู้ใช้ควรพิมพ์สูตรเองได้ **หรือ** ลากวางแบบไม่ต้องจำ syntax เลยก็ได้ — ต้องรองรับทั้งสองทาง -- สูตรต้องเลื่อนอ้างอิงถูกต้องเวลาคัดลอก/ลากลงทั้งคอลัมน์ (relative) **และ** เวลาแทรก/ลบแถว-คอลัมน์ - (structural) ซึ่งเป็นอัลกอริทึมคนละแบบกัน — พลาดจุดนี้ตารางจะคำนวณผิดโดยผู้ใช้ไม่รู้ตัว -- สูตรอ้างอิงกันเป็นวงกลมได้ ต้องตรวจจับไม่ให้ค้าง -- แก้ไขทุกอย่าง (พิมพ์ค่า, ใส่สูตร, จัดรูปแบบ) ต้อง undo/redo ได้ แต่การเลื่อน selection หรือสลับแท็บ - **ไม่ควร** นับเป็นประวัติที่ต้อง undo — ไม่งั้นกด Ctrl+Z ทีเดียวจะย้อนแค่ตำแหน่งเมาส์ ไม่ย้อนเนื้อหาจริง -- นำเข้า/ส่งออกไฟล์ Excel ต้องคง **สูตรต้นฉบับ + รูปแบบ (ตัวหนา/สี/จัดตำแหน่ง)** ไว้ ไม่ใช่แค่ค่าตัวเลข -- อยากรองรับทั้งคนไทยและคนอังกฤษ โดยไม่ต้องมี route แยกภาษา (`/en/...`) เพราะแอปนี้ render ฝั่ง client ล้วน +- ผู้ใช้ต้องพิมพ์สูตรเองได้ **และ** ลากวางสูตรได้โดยไม่ต้องรู้ไวยากรณ์ ทั้งสองทางต้องใช้งานได้ดีเท่ากัน +- สูตรต้องเลื่อนการอ้างอิงถูกต้องทั้งเมื่อคัดลอกหรือเติมลงทั้งคอลัมน์ (relative) **และ** เมื่อแทรกหรือลบแถวหรือคอลัมน์ + (structural) ซึ่งเป็นอัลกอริทึมคนละแบบกัน หากผิดพลาด ตารางจะคำนวณผิดโดยผู้ใช้ไม่ทราบ +- ต้องตรวจจับการอ้างอิงวนกลับ (circular reference) โดยแอปไม่ค้าง +- การแก้ไขทุกประเภท (พิมพ์ค่า ใส่สูตร จัดรูปแบบ) ต้อง undo/redo ได้ แต่การเลื่อนตำแหน่งที่เลือกหรือสลับชีต + ต้อง**ไม่**ถูกนับเป็นประวัติ มิฉะนั้น Ctrl+Z จะย้อนเพียงตำแหน่งเคอร์เซอร์แทนการแก้ไขจริง +- การนำเข้าและส่งออกไฟล์ Excel ต้องคง**สูตรต้นฉบับและรูปแบบ** (ตัวหนา สี การจัดตำแหน่ง) ไว้ ไม่ใช่เพียงค่าที่คำนวณแล้ว +- ต้องรองรับผู้ใช้ทั้งภาษาไทยและภาษาอังกฤษโดยไม่แยกเส้นทางตามภาษา (`/en/...`) เนื่องจากแอปเรนเดอร์ฝั่ง client ทั้งหมด -โฟกัสของโปรเจกต์จึงอยู่ที่ **ความถูกต้องของตรรกะคำนวณ + สถาปัตยกรรมที่ดูแลต่อได้** มากกว่าฟีเจอร์เยอะๆ -แบบผิวเผิน — ดูหัวข้อ [เอนจินคำนวณสูตร](#-เอนจินคำนวณสูตร) สำหรับรายละเอียดเชิงลึก +ด้วยเหตุนี้ โปรเจกต์จึงให้ความสำคัญกับ**ความถูกต้องของตรรกะการคำนวณและสถาปัตยกรรมที่ดูแลต่อได้** มากกว่าจำนวนฟีเจอร์ +รายละเอียดเชิงลึกอยู่ในหัวข้อ [เอนจินคำนวณสูตร](#เอนจินคำนวณสูตร) --- -## 🚀 วิธีรัน +## การติดตั้งและใช้งาน > ใช้เวลาประมาณ 2 นาที · ไม่ต้องมีฐานข้อมูลหรือ backend แยก ทุกอย่างรันอยู่ใน Next.js เดียว @@ -368,10 +353,10 @@ npm run dev | เส้นทาง | คืออะไร | |---|---| | `/` | landing page — อธิบายฟีเจอร์พร้อมภาพจากแอปจริง สองภาษาเหมือนตัวแอป | -| `/app` | ตัวโปรแกรมจริง เปิดมามีตารางตัวอย่างพร้อมใช้ทันที ไม่ต้องตั้งค่าอะไรเพิ่ม | +| `/app` | ตัวโปรแกรม เปิดมาเป็นตารางว่าง และมีปุ่ม **ลองกับข้อมูลตัวอย่าง** บนแถบสีเขียว ไม่ต้องตั้งค่าเพิ่มเติม | -(ข้อมูลตัวอย่างเป็นบิลขายกาแฟ/ขนมปัง/นม พร้อมสูตรคำนวณยอดรวมให้ดูของจริง กดชื่อ **ExcelToGo** -มุมซ้ายบนในแอปเพื่อกลับมาหน้า landing) +(ข้อมูลตัวอย่างเป็นรายการขายกาแฟ ขนมปัง และนม พร้อมสูตรคำนวณยอดรวมจริง กดชื่อ **ExcelToGo** +มุมซ้ายบนของแอปเพื่อกลับไปหน้า landing) คำสั่งอื่นๆ ที่มีให้: @@ -450,7 +435,7 @@ ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxx > และ serverless ลืมทุกครั้งที่ cold start **จึงกันการยิงมั่วได้ ไม่ใช่เครื่องมือคุมบิล** > > สิ่งที่คุมบิลได้มีอย่างเดียว: **ไม่ตั้ง key บน deployment สาธารณะ** — ตัวจับคำสำคัญในเครื่องตอบแทน ไม่มีค่าใช้จ่าย และ -> ผู้เข้าชมยังใส่ [key ของตัวเอง](#-ใส่-api-key-ของคุณเอง-byok) ได้ ซึ่งวิ่งจากเบราว์เซอร์ไปหา Anthropic ตรง ไม่แตะ route นี้ +> ผู้เข้าชมยังใส่ [key ของตัวเอง](#ใส่-api-key-ของคุณเอง-byok) ได้ ซึ่งวิ่งจากเบราว์เซอร์ไปหา Anthropic ตรง ไม่แตะ route นี้ > เว็บสาธารณะของโปรเจกต์นี้ตั้งใจให้เป็นแบบนั้น · เดิม `NEXT_PUBLIC_DEMO_MODE=1` ทำให้ route นี้ไม่เรียก Anthropic แม้มี key > **ตอนนี้ไม่แล้ว** (#109) — [`route.test.ts`](src/app/api/ai/formula/route.test.ts) คุมไว้ว่าตัวแปรเก่าไม่ซ่อน key ที่ตั้งไว้ > @@ -466,7 +451,7 @@ ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxx > **ไม่ได้ตั้ง `ANTHROPIC_API_KEY` และ `SOURCES_ADMIN_TOKEN`** บน deployment สาธารณะ — เดิมตัวแปรนี้คือสิ่งที่ทำให้ > สองตัวนั้นไม่มีผลถ้าตั้งค้างไว้ ตอนนี้ key มีผลทันทีที่ deploy เวอร์ชันนี้ และโทเคนมีผลทันทีที่ถอดตัวแปรออก -### 🔧 แก้ปัญหาที่พบบ่อย +### แก้ปัญหาที่พบบ่อย
กดดูวิธีแก้ @@ -485,9 +470,9 @@ ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxx --- -## ✨ ฟีเจอร์ +## ฟีเจอร์ -### 🤖 ถาม AI หาสูตร +### ถาม AI หาสูตร พิมพ์สิ่งที่ต้องการเป็นประโยคภาษาไทยหรืออังกฤษ เช่น _"อยากรวมยอดขายทั้งหมดในคอลัมน์นี้"_ ระบบส่งคำถาม พร้อมช่วงเซลล์ที่เลือกอยู่ไปให้ AI แนะนำสูตร พร้อมคำอธิบายสั้นๆ กดปุ่มเดียวเพื่อใส่สูตรลงเซลล์ทันที @@ -569,7 +554,7 @@ ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxx รันซ้ำได้ด้วย `npm run check:ai` (ต้องมี key ของคุณเอง ไม่อยู่ใน `npm run verify` เพราะมีค่าใช้จ่าย) -### 🔑 ใส่ API key ของคุณเอง (BYOK) +### ใส่ API key ของคุณเอง (BYOK) ผู้ช่วย AI ตอบได้สามทาง และหน้าแรกเขียนบอกไว้ตรงๆ ว่าเว็บสาธารณะใช้ทางไหน (ตัวจับคำสำคัญ หรือ key ของคุณเอง): @@ -598,7 +583,7 @@ key จะไปนั่งอยู่ในหน่วยความจำ > คำถาม ช่วงเซลล์ที่เลือก และชื่อหัวคอลัมน์ถูกส่งไปหา Anthropic เมื่อกดถามเท่านั้น — **ตัวไฟล์ไม่เคยถูกส่ง** > และถ้าไม่กด ก็ไม่มีอะไรออกจากเครื่องเลย -### 🔌 ข้อมูลสดจาก API / CSV (prototype) +### ข้อมูลสดจาก API / CSV (prototype)

เลือกแหล่งข้อมูลสด กดใส่ลงตาราง แล้วตัวเลขในตารางขยับเองทุก 5 วินาที

@@ -669,7 +654,7 @@ key จะไปนั่งอยู่ในหน่วยความจำ mixed content, กติกาเครือข่ายภายใน) กับ CSP ของหน้า ที่เปิดให้เฉพาะ origin ที่ผู้ใช้คนนั้นเพิ่มไว้ **API ที่ใหม่ = โหลดหน้าใหม่หนึ่งครั้ง** — `connect-src` ของ CSP แคบสำหรับทุกคน เพราะมันคือสิ่งที่กันไม่ให้สคริปต์แปลกปลอม -ส่ง [API key ของผู้ใช้](#-ใส่-api-key-ของคุณเอง-byok) ออกไปที่อื่น จะเปิดกว้างให้ทุกคนเพื่อฟีเจอร์นี้ไม่ได้ ตอนบันทึก +ส่ง [API key ของผู้ใช้](#ใส่-api-key-ของคุณเอง-byok) ออกไปที่อื่น จะเปิดกว้างให้ทุกคนเพื่อฟีเจอร์นี้ไม่ได้ ตอนบันทึก แอปจึงเขียน**แค่ origin** ของ URL (scheme + host + port ไม่มี path ไม่มี query) ลงคุกกี้ `etg-api-origins` แล้ว `src/proxy.ts` เติม origin นั้นลงใน policy ของ*ผู้ใช้คนนั้น*เท่านั้น แต่ policy ของหน้าถูกกำหนดตอนโหลด origin ใหม่จึงต้อง โหลดหน้าใหม่หนึ่งครั้ง ปุ่มบอกไว้ก่อนกด (**"อนุญาตและทดสอบ"** / **"บันทึก อนุญาต และใส่ลงตาราง"**) แอปโหลดให้เอง @@ -678,7 +663,7 @@ mixed content, กติกาเครือข่ายภายใน) กั คืองานที่อยู่แค่ในหน่วยความจำหาย - คนที่ไม่เคยเพิ่มแหล่งข้อมูลได้ CSP เดิม**ทุกตัวอักษร** — e2e เช็ก header จริงทั้งก่อนและหลังเพิ่ม - ลบแหล่งสุดท้ายของ origin ไหน origin นั้นก็ออกจากคุกกี้ -- ข้อแลกเปลี่ยนด้านความปลอดภัยเขียนไว้ตรง ๆ ใน[หัวข้อความปลอดภัย](#-ความปลอดภัย--ทดสอบอะไรไปบ้าง)และ [`SECURITY.md`](SECURITY.md) +- ข้อแลกเปลี่ยนด้านความปลอดภัยเขียนไว้ตรง ๆ ใน[หัวข้อความปลอดภัย](#ความปลอดภัย--ทดสอบอะไรไปบ้าง)และ [`SECURITY.md`](SECURITY.md) #### สิ่งที่ต้องขอ IT @@ -712,9 +697,9 @@ e2e มี flow ที่ชี้ไปหา API ที่ไม่ตอบ C #### ฐานข้อมูล / แหล่งฝั่งเซิร์ฟเวอร์ (self-host) เบราว์เซอร์ต่อ PostgreSQL หรือ MySQL ตรง ๆ ไม่ได้ทางเทคนิค (หน้าเว็บเปิด TCP ไปหาฐานข้อมูลเองไม่ได้) ทางที่ง่ายที่สุด -คือต่อผ่าน API ของบริษัทหรือ ERP ด้วยวิธีด้านบน ถ้าต้องการให้**เซิร์ฟเวอร์**เป็นคนดึงแทน — ต่อ[ฐานข้อมูลตรง](#-ต่อฐานข้อมูลตรง-ๆ-postgresql--mysql) +คือต่อผ่าน API ของบริษัทหรือ ERP ด้วยวิธีด้านบน ถ้าต้องการให้**เซิร์ฟเวอร์**เป็นคนดึงแทน — ต่อ[ฐานข้อมูลตรง](#ต่อฐานข้อมูลตรง-ๆ-postgresql--mysql) ซ่อน URL กับ credential จากคนใช้งาน หรือดึงจากที่ที่เซิร์ฟเวอร์ถึงแต่เครื่องผู้ใช้ไม่ถึง — ให้รันแอปบนเซิร์ฟเวอร์ของคุณเอง -([คู่มือในเว็บ](#-คู่มือต่อข้อมูลของคุณเอง-ในเว็บ) มีชุดเริ่มต้นสี่ขั้น) +([คู่มือในเว็บ](#คู่มือต่อข้อมูลของคุณเอง-ในเว็บ) มีชุดเริ่มต้นสี่ขั้น) **ส่วนนี้โผล่ใน UI เฉพาะเมื่อ deployment ตั้ง `SOURCES_ADMIN_TOKEN`** — root layout ตรวจทุก request แล้วทำเครื่องหมายไว้ บนหน้า (แค่ใช่/ไม่ใช่ ไม่ใช่ตัวโทเคน) ไม่ตั้ง = ไม่มีช่องโทเคน และไม่มีข้อความ "ฟีเจอร์นี้ถูกปิด" ให้ใครงง · route ฝั่งเซิร์ฟเวอร์ @@ -793,7 +778,7 @@ e2e มี flow ที่ชี้ไปหา API ที่ไม่ตอบ C ว่าง**หลายตัว** แอปไม่เดาว่าตารางคือตัวไหน บอกว่าอ่านเป็นตารางไม่ได้พร้อมรายชื่อคีย์ ให้ใส่ตำแหน่งข้อมูลใน JSON เอง · ใช้กับ ทั้งต่อจากเบราว์เซอร์และแหล่งฝั่งเซิร์ฟเวอร์ เพราะอยู่ในตัวแปลงชุดเดียวกัน -**แหล่งแบบ CSV กับรหัสที่ขึ้นต้นด้วยศูนย์** — แต่ละช่องของ CSV อ่านด้วย[กติกาเดียวกับตาราง](#-เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์) +**แหล่งแบบ CSV กับรหัสที่ขึ้นต้นด้วยศูนย์** — แต่ละช่องของ CSV อ่านด้วย[กติกาเดียวกับตาราง](#เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์) `007`, `0812345678` และตัวเลขล้วน 12 หลักขึ้นไปจึงมาเป็นข้อความตรงตัว ส่วน `12.5` กับ `-3` ยังเป็นตัวเลข (เดิมทุกช่องผ่าน `Number()` รหัสสาขา `007` จึงมาถึงตารางเป็น `7`) · แหล่งแบบ JSON ไม่เปลี่ยน: ชนิดข้อมูลมาจาก JSON เอง และข้อความ `"123"` ใน JSON ยังเป็นตัวเลขในตารางเหมือนเดิม @@ -876,7 +861,7 @@ e2e มี flow ที่ชี้ไปหา API ที่ไม่ตอบ C - **ยังไม่รองรับการยืนยันตัวตนด้วยคุกกี้หรือ SSO ของบริษัท** (`credentials: "include"`) **และยังไม่มี OAuth** — ใช้ได้เฉพาะ header ที่ใส่เอง - **ไม่มี "จำ token ไว้ในเครื่องนี้"** — ปิดแท็บแล้วต้องใส่ค่า header ใหม่ ตั้งใจแบบนี้ (ดูตารางด้านบน) - ตัวเลือกแบบ opt-in อยู่ใน[สิ่งที่จะทำต่อ](#-สิ่งที่จะทำต่อ) + ตัวเลือกแบบ opt-in อยู่ใน[สิ่งที่จะทำต่อ](#สิ่งที่จะทำต่อ) - **API ต้องเปิด CORS ให้เว็บที่เปิดอยู่** — แอปแก้แทนไม่ได้ ถ้าเจ้าของ API ไม่เปิด ทางที่เหลือคือแหล่งฝั่งเซิร์ฟเวอร์ - **API แบบ `http://` ที่ไม่ใช่ localhost ใช้จากเว็บ https ไม่ได้** — เบราว์เซอร์บล็อกเอง (mixed content) - **redirect ให้เบราว์เซอร์ตามเอง** — เบราว์เซอร์ทิ้ง `Authorization` เมื่อ redirect ข้าม origin แต่ header ชื่ออื่น @@ -884,10 +869,10 @@ e2e มี flow ที่ชี้ไปหา API ที่ไม่ตอบ C และทิ้ง header ทุกชื่อ — ฝั่งเบราว์เซอร์ทำแบบนั้นไม่ได้ เพราะหน้าเว็บอ่านปลายทางของ redirect ข้าม origin ไม่ได้ - **แหล่งฝั่งเซิร์ฟเวอร์ใช้บน Vercel ไม่ได้** — เก็บในไฟล์ `data/sources.json` ซึ่งดิสก์ของ Vercel เขียนไม่ได้ ต้องรันบน VPS, Docker ที่ผูก volume หรือเครื่องในบริษัท (ต่อจากเบราว์เซอร์ใช้บน Vercel ได้ปกติ เพราะไม่ได้เก็บอะไรบนเซิร์ฟเวอร์) -- **ยังไม่มีแหล่งตัวอย่างให้ลองทำตามบนเว็บ** — แหล่งตัวอย่างสามแหล่งที่เคยใส่ไว้ให้ถูกเอาออกแล้ว ตัวอย่างแบบทำตามทีละขั้น - อยู่ใน[สิ่งที่จะทำต่อ](#-สิ่งที่จะทำต่อ) +- **แหล่งตัวอย่างฝั่งเซิร์ฟเวอร์สามแหล่งเดิมถูกเอาออกแล้ว** และแทนที่ด้วย[API ตัวอย่างสามตัว](#ลองกับ-api-ตัวอย่าง) + ที่ต่อผ่านฟอร์มเดียวกับ API ของผู้ใช้ -### 🗄 ต่อฐานข้อมูลตรง ๆ (PostgreSQL / MySQL) +### ต่อฐานข้อมูลตรง ๆ (PostgreSQL / MySQL) เป็น[แหล่งฝั่งเซิร์ฟเวอร์](#ฐานข้อมูล--แหล่งฝั่งเซิร์ฟเวอร์-self-host) — ต้องรันแอปบนเซิร์ฟเวอร์ของคุณเองและตั้ง `SOURCES_ADMIN_TOKEN` เพราะเบราว์เซอร์ต่อฐานข้อมูลตรง ๆ ไม่ได้ · เหมือนแหล่งข้อมูลแบบ REST ฝั่งเซิร์ฟเวอร์ทุกอย่าง @@ -929,7 +914,7 @@ connection string ที่ชี้ไป **unix socket** ถูกปฏิเ ที่ต่อฐานข้อมูลจริง** — ส่วนที่แตะไดรเวอร์จึงถูกเขียนให้บางที่สุดเท่าที่ทำได้ และทุกการตัดสินใจอยู่ในโมดูลล้วน ที่ทดสอบได้โดยไม่ต้องมีฐานข้อมูล -### 📘 คู่มือต่อข้อมูลของคุณเอง (ในเว็บ) +### คู่มือต่อข้อมูลของคุณเอง (ในเว็บ) หน้า **`/guide`** คือที่ที่แอปส่งคนไปเมื่ออยากต่อ API หรือฐานข้อมูลของตัวเอง — จากเมนูบนมือถือ, จากลิงก์ท้ายแผงข้อมูลสด และจาก footer ของหน้า landing เดิมคำตอบอยู่แค่ใน README ซึ่งเป็นที่ที่คนที่รู้คำตอบอยู่แล้ว @@ -956,7 +941,7 @@ connection string ที่ชี้ไป **unix socket** ถูกปฏิเ (ยังไม่มีด่านไหนเทียบให้) -### 📐 ตารางกรอกข้อมูล (Spreadsheet Grid) +### ตารางกรอกข้อมูล (Spreadsheet Grid) - คลิกเลือกเซลล์, **ดับเบิลคลิก**/**F2** เพื่อแก้ไข, หรือพิมพ์ทับได้ทันทีโดยไม่ต้องดับเบิลคลิกก่อน - **แถบสูตร (formula bar)** แสดงที่อยู่ + เนื้อหาดิบของเซลล์ที่เลือกตลอดเวลาเหมือน Excel แก้ไขจากแถบนี้ได้เลย @@ -965,7 +950,7 @@ connection string ที่ชี้ไป **unix socket** ถูกปฏิเ - เลือกทั้งช่วงด้วยการลากเมาส์หรือ Shift+คลิก, คลิกหัวแถว/หัวคอลัมน์เพื่อเลือกทั้งแถว/คอลัมน์ - หัวแถว/หัวคอลัมน์ปักหมุด (sticky) และไฮไลต์สีฟ้าตามตำแหน่งที่เลือก — แก้ปัญหา "เลื่อนจอแล้วงงว่าอยู่แถวไหน" -### 💾 บันทึกอัตโนมัติ + Undo/Redo +### บันทึกอัตโนมัติ + Undo/Redo - ทุกการแก้ไข (พิมพ์ค่า, ใส่สูตร, จัดรูปแบบ, นำเข้าไฟล์) บันทึกลง `localStorage` ทันทีอัตโนมัติ ปิดแท็บ/รีเฟรช ข้อมูลยังอยู่ (ผูกกับเบราว์เซอร์/เครื่องนั้นเท่านั้น ไม่ sync ข้ามเครื่อง) · **งานที่ใหญ่เกินโควตาของเบราว์เซอร์ @@ -998,7 +983,7 @@ connection string ที่ชี้ไป **unix socket** ถูกปฏิเ

มือถือ: บรรทัดเดียวว่างานอยู่ในเบราว์เซอร์นี้ และกล่องจากสถานะบันทึกบนแถบบน มีข้อความเต็มกับปุ่มส่งออก Excel

-### 🩹 แอปพังแล้วยังเอาไฟล์ออกไปได้ +### แอปพังแล้วยังเอาไฟล์ออกไปได้ ถ้า render พังขึ้นมา สิ่งที่เคยเห็นคือหน้า error เปล่าๆ ของ Next: ไม่บอกว่าเกิดอะไรขึ้น ไม่มีทางกลับ และ — เรื่องที่สำคัญที่สุดสำหรับสเปรดชีต — ไม่มีอะไรบอกว่างานหายหรือยัง **มันไม่เคยหาย** ตารางอยู่ใน `localStorage` @@ -1051,7 +1036,7 @@ document ของตัวเอง **แปลว่าไม่มี stylesh origin ของ endpoint จะถูกใส่ใน `connect-src` ให้เอง — collector ที่ตั้งไว้แล้วแต่ไม่อยู่ใน policy จะถูกบล็อกเงียบ ๆ ซึ่งแย่กว่าการไม่มีเลย เพราะคนดูแลจะเชื่อว่ามีอยู่ -### ✂️ คัดลอก / ตัด / วาง +### คัดลอก / ตัด / วาง - **Ctrl+C / Ctrl+X / Ctrl+V** พร้อมเส้นประไฮไลต์บอกสถานะ (ฟ้า = คัดลอก, ส้ม = ตัด) - วางสูตรที่คัดลอกมา ระบบปรับการอ้างอิงแบบ relative ให้อัตโนมัติเหมือน Excel @@ -1066,10 +1051,10 @@ origin ของ endpoint จะถูกใส่ใน `connect-src` ให้ ย้ายทับตัวเองบางส่วนได้ ช่องที่ทิ้งไว้ถูกล้าง · สูตรที่อ้างเซลล์ปลายทางที่ถูกวางทับกลายเป็น `#REF!` แบบ Excel (ลองใน Excel จริงแล้ว) · **ยังไม่รองรับ:** ช่วงที่คร่อมเข้าไปในที่วางยังอยู่ที่เดิม และรูปแบบเซลล์ของช่องต้นทางยังอยู่ ไม่ได้ย้ายไปด้วย -### 🎨 จัดรูปแบบเซลล์ +### จัดรูปแบบเซลล์ ตัวหนา, **ตัวเอียง, ขีดเส้นใต้**, จัดตำแหน่งข้อความ (ซ้าย/กลาง/ขวา), สีข้อความ, รูปแบบตัวเลข (ทั่วไป / ทศนิยม 2 ตำแหน่ง / เปอร์เซ็นต์ / -สกุลเงิน ฿ / [ข้อความ](#-เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์)) — ติดไปกับเซลล์เวลาคัดลอก/วางและส่งออกเป็น Excel ด้วย +สกุลเงิน ฿ / [ข้อความ](#เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์)) — ติดไปกับเซลล์เวลาคัดลอก/วางและส่งออกเป็น Excel ด้วย

@@ -1086,7 +1071,7 @@ VAT = 0.07 จึงขึ้น "0.07%" ทั้งบนจอและใน ทั้งในเบราว์เซอร์และ workbook บนคลาวด์ เพราะถูกแปลงเป็นรหัส `0.00"%"` ที่แอปเคยใช้ตอนเปิดครั้งแรก เลือกเปอร์เซ็นต์ใหม่ เมื่อไรได้แบบ Excel -พิมพ์ `50%` ลงเซลล์ได้ตัวเลข 0.5 แล้ว (#52 · ดู[เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์](#-เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์)) +พิมพ์ `50%` ลงเซลล์ได้ตัวเลข 0.5 แล้ว (#52 · ดู[เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์](#เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์)) **ข้อจำกัด:** เศษส่วน (`# ?/?`) กับเลขวิทยาศาสตร์ (`0.00E+00`) ของไฟล์แสดงเป็น ตัวเลขธรรมดา · ตั้งรหัสรูปแบบเองในแอปยังไม่ได้ (มีแค่ที่มากับไฟล์) · workbook บนคลาวด์ที่บันทึกด้วยเวอร์ชันนี้ แอปเวอร์ชันเก่า @@ -1102,7 +1087,7 @@ VAT = 0.07 จึงขึ้น "0.07%" ทั้งบนจอและใน **`Ctrl+B` / `Ctrl+I` / `Ctrl+U`** (บน Mac ใช้ `Cmd`) สลับตัวหนา ตัวเอียง ขีดเส้นใต้ให้ทั้งช่วงที่เลือก โดยไม่เปิดเซลล์ขึ้นมาแก้ — เดิมกดแล้วตารางไม่ทำอะไรเลย ทั้งที่เป็นคีย์ลัดแรก ๆ ที่คนย้ายมาจาก Excel ลอง · อยู่ในหน้าคีย์ลัดหมวด "ตัวอักษร" แล้ว -### 🪣 สีพื้นเซลล์ +### สีพื้นเซลล์ ปุ่ม**ถังสี**ในแถบจัดรูปแบบเปิดช่องสีเจ็ดสีกับ "ไม่มีสี" ใช้กับทั้งช่วงที่เลือก ติดไปกับการคัดลอก/วางและการส่งออก `.xlsx` และ undo ได้ในก้าวเดียว แถบใต้ไอคอนถังเป็นสีของเซลล์ที่เลือกอยู่ และในช่องสีจะมีกรอบบอกว่าสีไหนคือสีปัจจุบัน @@ -1114,11 +1099,11 @@ VAT = 0.07 จึงขึ้น "0.07%" ทั้งบนจอและใน หัวตารางจึงยังอ่านออก และไฟล์ที่ส่งกลับไปเปิดใน Excel ก็ดูเข้ากัน ส่วนวงล้อสี (``) บนมือถือเป็น เป้าเล็ก ๆ ที่ชวนให้เลือกสีเข้มไว้ใต้ตัวอักษรสีเข้ม -สีพื้นกับสีตัวอักษร**ออกไปกับการส่งออก PDF ด้วยแล้ว** (ดู[ส่งออกไฟล์](#-ส่งออกไฟล์)) +สีพื้นกับสีตัวอักษร**ออกไปกับการส่งออก PDF ด้วยแล้ว** (ดู[ส่งออกไฟล์](#ส่งออกไฟล์)) **ยังไม่รองรับ:** เลือกสีเองนอกเจ็ดสีนี้ (สีจากไฟล์ที่นำเข้ายังแสดงและส่งออกได้ครบ) -### 🔢 เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์ +### เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์ พิมพ์ `0812345678`, เลขบัตรประชาชน `1234567890123` หรือรหัสสินค้า `00123` แล้ว**ได้ตรงตามที่พิมพ์** — ในตาราง, ในไฟล์ `.xlsx`, CSV, PDF, ตอนเรียง, ตอนกรอง, ใน pivot และในกราฟ เดิมเลขศูนย์นำหน้าหายแบบเงียบ ๆ ในทุกทางออก @@ -1160,11 +1145,11 @@ conditional formatting ไม่นับมันเป็นตัวเลข - **กู้ของที่หายไปแล้วไม่ได้** — ไฟล์ `.xlsx` ที่เคยส่งออกหรือนำเข้ามาก่อนแก้ เก็บ `812345678` เป็นตัวเลขไปแล้ว ไม่มีศูนย์ให้คืน (ข้อมูลสดแบบ CSV ใช้กติกาเดียวกันแล้วตั้งแต่ #36) - **`=SUM(A1:A3)` บนรหัสเหล่านี้ได้ 0 แบบ Excel แล้ว (#166, #38)** — ข้อความที่มาจากเซลล์ไม่ถูกบวก - ยอดรวมของชีตเก่าที่มีรหัสปนอยู่ในช่วงจึงเปลี่ยนไปเป็นตัวเลขเดียวกับที่ Excel ให้ (ดู [เอนจินคำนวณสูตร](#-เอนจินคำนวณสูตร)) + ยอดรวมของชีตเก่าที่มีรหัสปนอยู่ในช่วงจึงเปลี่ยนไปเป็นตัวเลขเดียวกับที่ Excel ให้ (ดู [เอนจินคำนวณสูตร](#เอนจินคำนวณสูตร)) - รูปแบบ "ข้อความ" เป็นรูปแบบเซลล์ จึงยังไม่ซิงก์สดตอนแก้พร้อมกันหลายคน (ข้อจำกัดเดิมของรูปแบบทุกชนิด) ส่วน apostrophe อยู่ในตัวเซลล์ จึงซิงก์ได้ -### 📅 วันที่และเวลา +### วันที่และเวลา พิมพ์ `2024-01-15` แล้ว**ได้วันที่จริง** ไม่ใช่ข้อความที่หน้าตาเหมือนวันที่ — ค่าของมันคือเลข serial แบบเดียวกับ Excel (`2024-01-15` = 45306, เที่ยงวัน = .5) `=A2-A1` จึงได้จำนวนวัน, `=A1+30` ได้วันที่อีกเดือนข้างหน้า และไฟล์ที่ส่งออก @@ -1181,7 +1166,7 @@ conditional formatting ไม่นับมันเป็นตัวเลข

วันที่ในตาราง: ลบกันได้จำนวนวัน, วันที่และเวลาในคอลัมน์ที่ขยายให้พอดี, เวลาล้วน และ =DATE(2024,3,1) ในแถบสูตรที่เซลล์แสดงเป็น 2024-03-01 พร้อมเมนูรูปแบบที่ขึ้นว่า วันที่

- **รับเฉพาะรูปแบบ ISO** (`ปปปป-ดด-วว`) — `03/04/2024` เป็นมีนาคมในประเทศหนึ่งและเมษายนในอีกประเทศหนึ่ง การเดาผิด - คือการเลื่อนวันที่ไปหนึ่งเดือนโดยไม่มีใครรู้ ข้อความแบบอื่นจึงยังเป็นข้อความตามที่พิมพ์ — ยกเว้นรูปแบบไทยที่อ่านผิดไม่ได้ ดู [วันที่แบบ พ.ศ.](#-วันที่แบบ-พศ) + คือการเลื่อนวันที่ไปหนึ่งเดือนโดยไม่มีใครรู้ ข้อความแบบอื่นจึงยังเป็นข้อความตามที่พิมพ์ — ยกเว้นรูปแบบไทยที่อ่านผิดไม่ได้ ดู [วันที่แบบ พ.ศ.](#วันที่แบบ-พศ) - **ปี ค.ศ. 1900 ตามแบบ Excel** รวมวันที่ 29 ก.พ. 1900 ที่ไม่มีอยู่จริง (Excel รับมาจาก Lotus 1-2-3) — วันที่ที่พิมพ์ ที่นี่จึงเป็นเลขเดียวกับใน Excel ทุกวัน - **เปิดไฟล์ `.xlsx`** ได้วันที่พร้อมเวลา เซลล์ที่มีแต่เวลาเป็น `09:45` (เดิมกลายเป็น `1899-12-30`) และเซลล์ที่ไฟล์ @@ -1208,7 +1193,7 @@ conditional formatting ไม่นับมันเป็นตัวเลข - **ผลของสูตรไม่ได้ตั้งรูปแบบเองทุกแบบ** — `=A1+30` ที่ A1 เป็นวันที่แสดงเป็นเลข serial จนกว่าจะเลือกรูปแบบ "วันที่" (Excel เดาจากสูตร เรายังไม่เดา) มีแค่สูตรที่ขึ้นต้นด้วย `TODAY`, `NOW` หรือ `DATE` ที่ได้รูปแบบให้เอง - **ข้อความวันที่ที่กำกวมยังเป็นข้อความ** ทั้งที่พิมพ์เอง, ที่วางมา, ใน CSV และในข้อมูลสด — `15/01/2024` ลบกันไม่ได้ - จนกว่าจะใช้คำสั่ง [แปลงเป็นวันที่](#-วันที่แบบ-พศ) (#82) + จนกว่าจะใช้คำสั่ง [แปลงเป็นวันที่](#วันที่แบบ-พศ) (#82) - **ข้อความ `d/m/yyyy` ปี ค.ศ. ได้ `#VALUE!` ในฟังก์ชันวันที่ด้วย (#169)** — `=DAY(A1)` ที่ A1 เป็นข้อความ `1/9/2026` ได้ `#VALUE!` เหมือน `=A1+1` ทั้ง `DAY` `MONTH` `YEAR` และ `DATEDIF` · เดิมสี่ฟังก์ชันนี้อ่านเองแบบสหรัฐ (9 ม.ค.) และเลื่อนตามเขตเวลา ของเครื่อง จึงได้ 8 ที่กรุงเทพฯ และ 9 ที่นิวยอร์ก โดยไม่มีอะไรเตือน · ใช้คำสั่งแปลงเป็นวันที่ก่อน แล้วทุกสูตรนับได้ @@ -1217,7 +1202,7 @@ conditional formatting ไม่นับมันเป็นตัวเลข - การเลือกรูปแบบวันที่เป็นรูปแบบเซลล์ จึงยังไม่ซิงก์สดตอนแก้พร้อมกันหลายคน (ข้อจำกัดเดิมของรูปแบบทุกชนิด) ส่วนค่าวันที่อยู่ในตัวเซลล์ จึงซิงก์ได้ -### 📆 วันที่แบบ พ.ศ. +### วันที่แบบ พ.ศ. ไฟล์ของคนไทยเก็บวันที่เป็น `15/01/2569`, `15 ม.ค. 2569` หรือ `15/01/69` บ่อยมาก (feedback จากผู้ใช้จริง, #82) ถ้าอ่านตรงตัว ทุกวันที่จะคลาดไป 543 ปี และ `DATEDIF` อายุงาน ระยะค้างชำระ การเรียงตามวันที่ ผิดหมดโดยไม่มีอะไรฟ้อง @@ -1259,7 +1244,7 @@ conditional formatting ไม่นับมันเป็นตัวเลข และคำสั่งแปลงเป็นวันที่ทำให้เป็นวันที่จริง - ยังไม่รับเลขไทย (`๑๕/๐๑/๒๕๖๙`) และเวลาแบบ `13.45 น.` -### 📊 กราฟจากข้อมูลในตาราง +### กราฟจากข้อมูลในตาราง เลือกช่วง กดปุ่ม **กราฟ** ในแถบรูปแบบ แล้วเลือก **แท่ง / เส้น / วงกลม** — กราฟไปวางอยู่บนตารางใต้ช่วงที่อ่าน **ลากแถบบนหัวกราฟเพื่อย้าย ลากมุมขวาล่างเพื่อย่อขยาย** สลับชนิดหรือลบได้จากบนตัวกราฟเอง @@ -1313,7 +1298,7 @@ Excel จะบอกว่า "เสียหาย" แล้วเปิด **ยังไม่รองรับ:** กราฟใน PDF ยังเป็นรูปภาพ (jsPDF ไม่มีกราฟของตัวเอง) · กราฟวงกลมยังวาดได้ทีละชุดข้อมูล · ไม่ได้ใส่สี/ธีมลงใน chart part ปล่อยให้โปรแกรมที่เปิดใช้ค่าเริ่มต้นของตัวเอง -### ☁️ บันทึกบนคลาวด์ (ต่อ backend ของคุณเอง) +### บันทึกบนคลาวด์ (ต่อ backend ของคุณเอง) **ปิดอยู่โดยค่าเริ่มต้น** และตั้งใจให้เป็นแบบนั้น ExcelToGo เป็นโปรเจกต์โอเพนซอร์ส **ไม่ใช่บริการที่มีคนโฮสต์ให้** การเปิดฐานข้อมูลกลางให้ทุกคนใช้ จะทำให้ผู้ดูแลกลายเป็นผู้ควบคุมข้อมูลส่วนบุคคลพร้อมภาระตามกฎหมาย และกลับด้าน @@ -1373,7 +1358,7 @@ policy จากคนที่ไม่ได้ล็อกอิน (ก่ **ยังไม่รองรับ:** sync อัตโนมัติ (กดบันทึกเอง) -### 👥 แก้พร้อมกันหลายคน +### แก้พร้อมกันหลายคน บันทึกงานขึ้น Supabase ของคุณ แชร์ให้คนที่ต้องการ แล้วกด **"เข้าห้องแก้ร่วมกัน"** ในแถบคลาวด์ ทุกคนที่เปิดงานนั้นได้จะเห็นการพิมพ์ของกันและกันทันที พร้อมจุดสีบอกว่าใครอยู่ช่องไหน @@ -1413,7 +1398,7 @@ JavaScript ให้ทุกคนอ่าน workbook id เป็น uuid ข้อความทุกชิ้นที่มาจากช่องสัญญาณ**ยังถูกตรวจก่อนใช้อยู่ดี** เพราะการออกสิทธิ์บอกว่าใครพูดได้ ไม่ได้บอกว่า สิ่งที่พูดมาถูกรูปแบบ — `row` ที่เป็น `-1` จะไปถึง array index และ `raw` ที่ไม่ใช่สตริงจะไปถึงเอนจินคำนวณสูตร -### 💬 คอมเมนต์ในเซลล์ +### คอมเมนต์ในเซลล์ เลือกเซลล์ กด **คอมเมนต์** ในแถบรูปแบบ แล้วพิมพ์โน้ตไว้ เซลล์ที่มีโน้ตจะมี**มุมสีส้ม** ชี้เมาส์ค้างเพื่ออ่าน @@ -1436,7 +1421,7 @@ JavaScript ให้ทุกคนอ่าน workbook id เป็น uuid **โน้ตบนเซลล์ที่ว่างเปล่าจะหายตอนนำไฟล์กลับเข้ามา** — ตัวไฟล์ถูกต้อง (Excel เห็นโน้ตนั้น) แต่ ExcelJS ผูกโน้ตกับเซลล์ที่มีอยู่ในโมเดลเท่านั้น และเซลล์ที่ไม่มีค่าก็ไม่มีอยู่ในนั้น จุดที่หายคือฝั่งอ่าน ไม่ใช่ฝั่งเขียน -### 🛡 จำกัดสิ่งที่กรอกได้ (Data Validation) +### จำกัดสิ่งที่กรอกได้ (Data Validation) เลือกช่วง กด **จำกัดค่า** ในแถบรูปแบบ แล้วบอกว่าช่องเหล่านั้นรับอะไรได้ — เลือกจากรายการ, ตัวเลขในช่วง, หรือความยาวไม่เกินกี่ตัวอักษร ช่องที่มีกติกาจะมี**ขอบสีเขียวบาง ๆ** และช่องที่เป็นรายการจะเปิดเป็น dropdown @@ -1479,7 +1464,7 @@ dropdown ที่ชี้ไปช่วงเซลล์ใน*ชีตอ กฎ `whole`/`decimal`/`textLength`/`date` บนชีตที่ล็อกหายหมด — ทั้งหมดต้องแก้ก่อน [แม่แบบนำเข้าของ PaynEat ERP](docs/payneat-erp.md) -### 🏷 ตั้งชื่อช่วงข้อมูล (Named Ranges) +### ตั้งชื่อช่วงข้อมูล (Named Ranges) `=SUMIF(ยอดขาย,">1000")` กับ `=SUMIF(B2:B500,">1000")` ต่างกันตรงที่อย่างหลังต้องไปเปิดดูว่าคอลัมน์ B คืออะไรทุกครั้ง และคนที่เดาผิดก็ไม่มีทางรู้ แถบสูตรคือที่ที่สเปรดชีตอธิบายตัวเอง และที่อยู่เซลล์ไม่ได้อธิบายอะไรเลย @@ -1512,7 +1497,7 @@ dropdown ที่ชี้ไปช่วงเซลล์ใน*ชีตอ **เข้า/ออกไฟล์ `.xlsx` เป็น defined name ของจริง พร้อมขอบเขตเดิม** ชื่อระดับไฟล์ยังเป็นระดับไฟล์ ชื่อที่มี `localSheetId` ยังเป็นของชีตที่ไฟล์บอก (ไม่ใช่ชีตที่มันชี้ไป) และส่งออกแล้วเปิดกลับมาได้ขอบเขตเดิม · **ชื่อที่นำเข้าไม่ได้ -จะถูกบอกชื่อตอนเปิดไฟล์** ในรายงานเดียวกับสิ่งอื่นที่ไฟล์เก็บไว้ไม่ได้ ([#83](#-บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้)) — ชื่อที่เป็นสูตร (`=OFFSET(...)`), ทั้งคอลัมน์ (`$B:$B`), หลายช่วง หรือชี้ชีตที่ไม่มีในไฟล์ — +จะถูกบอกชื่อตอนเปิดไฟล์** ในรายงานเดียวกับสิ่งอื่นที่ไฟล์เก็บไว้ไม่ได้ ([#83](#บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้)) — ชื่อที่เป็นสูตร (`=OFFSET(...)`), ทั้งคอลัมน์ (`$B:$B`), หลายช่วง หรือชี้ชีตที่ไม่มีในไฟล์ — แทนที่จะกลายเป็น `#NAME?` เงียบ ๆ **ข้อจำกัดที่เขียนไว้ตรง ๆ:** ชื่อที่เป็นสูตรหรือทั้งคอลัมน์ยังนำเข้าไม่ได้ (แต่บอก) · ถ้าเพิ่มไฟล์ต่อท้ายงานเดิมแล้ว @@ -1520,7 +1505,7 @@ dropdown ที่ชี้ไปช่วงเซลล์ใน*ชีตอ ชี้ไปที่ที่ไม่มีใครขอ · ลบชีตที่ชื่อระดับไฟล์ถูกเก็บไว้ ชื่อนั้นหายไปด้วย · **ลบชื่อแล้วสูตรยังถือชื่อนั้นไว้** และขึ้น `#NAME?` ไม่ใช่ถูกเขียนกลับเป็นที่อยู่ — การแก้สูตรให้เอง โดยไม่มีใครสั่งเป็นสิ่งที่แย่กว่า และ `#NAME?` ก็ทั้งค้นหาเจอและ undo ได้ -### 🌡 จัดรูปแบบตามเงื่อนไข (Conditional Formatting) +### จัดรูปแบบตามเงื่อนไข (Conditional Formatting) ให้สีเปลี่ยนตามตัวเลข แทนที่จะทาสีทิ้งไว้แล้วค้างอยู่อย่างนั้นเวลาค่าเปลี่ยน — เลือกช่วง กดปุ่ม **จัดรูปแบบตามเงื่อนไข** ในแถบรูปแบบ แล้วเพิ่มกฎได้ 5 แบบ @@ -1546,7 +1531,7 @@ dropdown ที่ชี้ไปช่วงเซลล์ใน*ชีตอ ส่งออก PDF ก็ยังไม่รองรับ) · เมื่อกฎหลายข้อชนกันที่เซลล์เดียว **ข้อล่างทับข้อบน** ซึ่งกลับทางกับ Excel ที่ให้ลำดับบนสุดชนะ — เลือกแบบนี้เพราะกฎที่เพิ่งกดเพิ่มควรเห็นผลทันที ไม่ใช่เงียบหายไป -### ⌨️ คีย์บอร์ดแบบเดียวกับ Excel +### คีย์บอร์ดแบบเดียวกับ Excel คนที่เปิดโปรแกรมตารางขึ้นมา มือจะไปก่อนตาเสมอ — กด `Ctrl+ลูกศรลง` ตั้งแต่ยังไม่ได้อ่านอะไรบนหน้าจอเลย ถ้าตารางตอบด้วยการเลื่อนลงหนึ่งแถว มันบอกคนกดทันทีว่านี่เป็นแค่ของจำลอง @@ -1565,7 +1550,7 @@ dropdown ที่ชี้ไปช่วงเซลล์ใน*ชีตอ | `Ctrl+Enter` | เอาค่าในช่องที่เคอร์เซอร์อยู่ ใส่ลงทุกช่องที่เลือก ใน undo ก้าวเดียว — การอ้างอิงขยับตามเหมือนลากเติม เพราะสูตรที่ยังชี้แถวเดิมจะทำให้ได้คอลัมน์ที่เป็นเลขผิดเหมือนกันหมด | | `Tab` / `Shift+Tab` · `Enter` / `Shift+Enter` | ไปขวา/ซ้าย · ลง/ขึ้น — **กด `Tab` ไปตามแถวแล้ว `Enter` จะกลับมาใต้คอลัมน์ที่เริ่ม** แบบ Excel พิมพ์ตารางทีละแถวได้โดยไม่ต้องกดกลับไปซ้ายเอง · **`Tab` ที่คอลัมน์สุดท้ายเพิ่มคอลัมน์ให้** แทนการค้างอยู่ที่เดิมแล้วพิมพ์ทับค่าที่เพิ่งใส่ (บนแม่แบบที่เพิ่มคอลัมน์ไม่ได้ จะทำแบบ Enter) · **`Enter` หรือ `↓` ที่แถวสุดท้ายเพิ่มแถวให้ (#170)** ชีตยาวขึ้นตามที่พิมพ์ลงไปเรื่อย ๆ และ undo ครั้งเดียวเอาค่าที่พิมพ์ในแถวใหม่ออกพร้อมแถวนั้น (บนแม่แบบที่เพิ่มแถวไม่ได้ เคอร์เซอร์ยังอยู่ที่เดิม) · บนมือถือ ถ้ากด undo ตอนช่องแก้ไขยังเปิดอยู่บนแถวที่ undo เอาออก ข้อความที่ยังไม่กด Enter จะถูกทิ้งและแอปบอกให้รู้ (#191) | | ปุ่มบนแถบเครื่องมือ | **กดแล้วเคอร์เซอร์ยังอยู่ที่ตาราง** — ลูกศรกับ Delete ใช้ต่อได้ทันที ไม่ต้องคลิกเซลล์ใหม่ (ปุ่มยังเข้าถึงได้ด้วย Tab ตามปกติ) | -| `Shift+F10` / ปุ่ม Menu | [เมนูเซลล์](#️-คลิกขวาที่เซลล์) สำหรับคนที่คลิกขวาไม่ได้ เปิดตรงเซลล์ที่เลือก | +| `Shift+F10` / ปุ่ม Menu | [เมนูเซลล์](#คลิกขวาที่เซลล์) สำหรับคนที่คลิกขวาไม่ได้ เปิดตรงเซลล์ที่เลือก | | `F2` · `Delete` · `Escape` | แก้ในช่อง · ล้างช่วงที่เลือก · ยกเลิก | หน้าจอ**วิ่งตามเคอร์เซอร์เองทั้งแนวตั้งและแนวนอน** — จำเป็นจริง ๆ เพราะ `Ctrl+ลูกศรลง` กระโดดได้ทีละห้าพันแถว @@ -1599,7 +1584,7 @@ dropdown ที่ชี้ไปช่วงเซลล์ใน*ชีตอ - `npm run check:a11y` เขียวมาตลอด เพราะ axe เห็นแค่ `/app` ตอนโหลด และหน้านี้ยังไม่มีตัวตนจนกว่าจะมีคนกด พอรัน axe ใส่ตอนมันเปิดอยู่ เจอ 2 violation ระดับ serious ข้างใน — หัวข้อกลุ่มคอนทราสต์ 2.62:1 และ รายการที่เลื่อนได้แต่คีย์บอร์ดเข้าไม่ถึง แก้แล้วทั้งคู่ และ**ตอนนี้ด่านเปิดหน้านี้ขึ้นมาตรวจด้วย** ครั้งหน้า - CI จะจับได้เอง ไม่ต้องรอให้ผมนึกขึ้นได้ + CI จะตรวจพบเองโดยไม่ต้องอาศัยการตรวจด้วยมือ **แต่โฟกัสบอกได้แค่ว่าเคอร์เซอร์อยู่ตรงไหน** เรียงคอลัมน์แล้วแถวสลับที่กันใต้เคอร์เซอร์ที่ไม่ได้ขยับ วางข้อมูลแล้ว สี่สิบช่องเต็มขึ้นมาใต้ขอบจอ กรองแล้วครึ่งตารางหายไป ทั้งหมดนี้เคยเงียบสนิท ตอนนี้มี live region แบบ polite @@ -1644,7 +1629,7 @@ dropdown ที่ชี้ไปช่วงเซลล์ใน*ชีตอ **ยังไม่มี screen reader ตัวจริงได้ฟังสักที** — สิ่งที่ยืนยันได้คือข้อความที่ถูกไปถึง region ที่ถูก ในจังหวะที่ถูก ทั้งสองภาษา -### 🧮 สูตรที่ตอบมาเป็นทั้งตาราง (array formula) +### สูตรที่ตอบมาเป็นทั้งตาราง (array formula) สูตรส่วนใหญ่ตอบมาค่าเดียว บางสูตรตอบมาเป็น*รูปทรง* — `=UNIQUE(B2:B10)` มีคำตอบเท่ากับจำนวนหมวดหมู่ ที่ไม่ซ้ำกัน ซึ่งรู้ไม่ได้ตอนพิมพ์ คำตอบพวกนี้จึง **ล้นลงเซลล์ข้าง ๆ เอง** เซลล์ที่โดนล้นใส่มีสีจาง ๆ กำกับไว้ @@ -1690,7 +1675,7 @@ array ที่หดจึงไม่ทิ้งอะไรค้างไ > ส่วนไฟล์ `.xlsx` ที่ส่งออกเขียนสูตรไว้ที่เซลล์ต้นทาง Excel รุ่นที่รู้จัก dynamic array จะล้นให้เอง > รุ่นเก่ากว่านั้นจะเห็นแค่ค่าเดียว -### 🔗 สูตรข้ามชีต +### สูตรข้ามชีต `=Sheet2!A1` เคยขึ้น `#SYNTAX!` แอปมีแท็บหลายชีตและมี pivot ที่อ่านชีตต้นทางมาตั้งแต่ต้น แต่สำหรับสูตรแล้ว ทุกชีตเป็นเกาะ และไฟล์ `.xlsx` จริงที่มีสูตรข้ามชีตอยู่ นำเข้ามาก็กลายเป็น error @@ -1728,7 +1713,7 @@ sort และประโยคที่ filter พูดออกมา คำ เรียก `computeSheet` เปล่า ๆ นอกเอนจิน และ cache เก็บผลไว้คู่กับสิ่งที่มันอ่านจากชีตอื่น จึงเช็กได้ทุกครั้งว่ายังใช้ได้ ไม่ใช่เฉพาะตอนที่ยังอยู่ในประวัติสี่อันล่าสุด -### 🔍 ดูว่าสูตรนี้พูดถึงช่องไหน +### ดูว่าสูตรนี้พูดถึงช่องไหน เลือกเซลล์ที่เป็นสูตร แล้วช่องที่มันอ่านจะถูกตีกรอบบนตาราง พร้อมเขียนช่วงเป็นตัวอักษรไว้ข้างแถบสูตร @@ -1750,7 +1735,7 @@ dependency graph คำนวณเรื่องนี้อยู่แล้ ทั้งจอเป็นสีเดียว · ขนาดถูกนับ*ก่อน*สร้าง set การอ้างอิงทั้งคอลัมน์จึงไม่จองหน่วยความจำล้านช่องระหว่างทางไปสู่การถูกปฏิเสธ - **ไม่แสดงตอนเปิดช่องแก้อยู่** เพราะข้อความเปลี่ยนทุกคีย์สโตรก และตอนนั้นปุ่มเป้าเล็งทำหน้าที่นี้ได้ดีกว่าอยู่แล้ว -### 🖱️ ลากมุมเพื่อเติม (fill handle) +### ลากมุมเพื่อเติม (fill handle) สิ่งแรกที่คนทำกับสเปรดชีตคือลากมุมขวาล่าง แอปนี้มีจุดจับที่มุมอยู่แล้ว — ระบบสัมผัสใช้มันขยายช่วงที่เลือก — แต่บนเมาส์ไม่มีอะไรอยู่ข้างหลังมันเลย @@ -1792,7 +1777,7 @@ Item 08 → Item 09, Item 10 คงเลขศูนย์นำหน้ และการเติมจะเกิดตอนปล่อยเมาส์ ไม่ใช่ระหว่างลาก ซึ่งสำคัญกับ undo พอ ๆ กับความสบายใจ — ลากหนึ่งครั้ง คือย้อนหนึ่งครั้ง ไม่ใช่ย้อนทีละเซลล์ที่ลากผ่าน -### 🔎 ค้นหาและแทนที่ +### ค้นหาและแทนที่ `Ctrl+F` เป็นปฏิกิริยาอัตโนมัติ และที่นี่มันไม่ทำอะไรเลย — ซึ่งยิ่ง*สะดุด*ขึ้นไปอีก ไม่ใช่น้อยลง ตอนที่หน้าคีย์ลัดเข้ามาพร้อมคำโฆษณาว่า "ปุ่มชุดเดียวกับ Excel" @@ -1812,7 +1797,7 @@ Item 08 → Item 09, Item 10 คงเลขศูนย์นำหน้ และมันจงใจแย่งแถบค้นหาของเบราว์เซอร์ เพราะแถบนั้นค้นจาก DOM ซึ่งเก็บแค่สี่สิบแถวที่ตารางตัดสินใจเรนเดอร์ — บนชีตห้าพันแถวมันจะรายงานว่า "ไม่พบ" ทั้งที่ข้อความอยู่ตรงนั้นชัด ๆ -### 📴 เปิดได้ตอนไม่มีเน็ต +### เปิดได้ตอนไม่มีเน็ต คำโปรยของแอปนี้พูดมาตลอดว่าสเปรดชีตอยู่ในเบราว์เซอร์คุณ ไม่ถูกอัปโหลดไปไหน — ซึ่งจริง แต่แอปก็ยังเปิดบนรถไฟไม่ได้อยู่ดี เอกสารอยู่ในเครื่อง แต่*ตัวโปรแกรม*ไม่ได้อยู่ เป็นเรื่องที่คนติได้อย่างเป็นธรรม @@ -1840,7 +1825,7 @@ worker ยาวประมาณร้อยบรรทัด เขียน ไม่มีภาพหน้าจอ เพราะกล่องติดตั้งเป็น chrome ของเบราว์เซอร์เองและหน้าตาไม่เหมือนกันสักตัว — ด่าน e2e ดูแลแทน: ลงทะเบียน worker, ปิดเน็ต, โหลดใหม่ แล้วเช็กว่าเห็นตารางไม่ใช่หน้า error -### 📱 ใช้บนมือถือได้ +### ใช้บนมือถือได้ เดิมเปิดบนมือถือแล้ว**ไม่เห็นตารางเลยสักช่อง** — แถบสูตรด้านข้างกว้าง 320px แย่งพื้นที่จนเหลือแค่คอลัมน์เลขแถว และแถบเครื่องมือแตกเป็น 3 บรรทัดกินไป 380px จาก 844px ก่อนถึงตาราง ตอนนี้แก้แล้วทั้งสามเรื่อง: @@ -1853,7 +1838,7 @@ worker ยาวประมาณร้อยบรรทัด เขียน | **แก้ค่าในเซลล์** | **แตะครั้งแรกเลือก แตะซ้ำเพื่อแก้** — เดิมต้องดับเบิลคลิกซึ่งมือถือทำไม่ได้ จึงพิมพ์อะไรไม่ได้เลย · แตะซ้ำแล้ว**เคอร์เซอร์ไปอยู่ท้ายข้อความ** ไม่ใช่เลือกทั้งหมด (เดิมตัวอักษรแรกที่พิมพ์ลบของเดิมทั้งช่อง) · **กด Enter แล้วเข้าช่องถัดไปพร้อมพิมพ์ต่อ คีย์บอร์ดไม่พับ** — เดิมพับทุกครั้ง กรอกสิบช่องต้องแตะเปิดใหม่สิบครั้ง · ถ้าช่องถัดไปมีค่าอยู่แล้ว **ค่าเดิมถูกเลือกไว้ พิมพ์แล้วแทนที่** เหมือนบนเดสก์ท็อปและ Excel (#202 เดิมไปต่อท้าย 599 กลายเป็น 599650) ส่วนแตะซ้ำยังแก้ต่อท้ายได้เหมือนเดิม · **แตะครั้งเดียวแล้วเซลล์บอกว่า "แตะอีกครั้งเพื่อพิมพ์"** และคีย์บอร์ดที่ค้างจากช่องก่อนหน้าพับไป (#171 ดูด้านล่าง) | | **ขนาดปุ่ม** | 44×44 ทุกปุ่มในทั้งสองแถบ (เดิม 28px — และตอนหลังยังเหลือ 32px กว้างอยู่ห้าปุ่มในแถบรูปแบบ วัดเจอแล้วแก้) | | **เลือกหลายเซลล์** | **จุดจับที่มุมขวาล่างของช่วง** ลากเพื่อขยายช่วง — พื้นที่รับนิ้ว 44×44 (จุดที่เห็นยังเล็กเท่าเดิม) | -| **คัดลอก / วาง / เติมลง** | **แถบคำสั่งเหนือแท็บชีต** — ดู[คัดลอก วาง เติมลงด้วยนิ้ว](#-คัดลอก-วาง-เติมลงด้วยนิ้ว) | +| **คัดลอก / วาง / เติมลง** | **แถบคำสั่งเหนือแท็บชีต** — ดู[คัดลอก วาง เติมลงด้วยนิ้ว](#คัดลอก-วาง-เติมลงด้วยนิ้ว) | | **ปุ่มที่โผล่ตอน hover** | บนจอสัมผัสไม่มี hover จึง**แสดงถาวร** — ไม่งั้นปุ่มกรองคอลัมน์กับปุ่มลบชีตจะมองไม่เห็นเลย | ![ใช้บนมือถือ](public/screenshots/19-mobile.png) @@ -1874,7 +1859,7 @@ worker ยาวประมาณร้อยบรรทัด เขียน
ปุ่มในแผ่นเครื่องมือเซลล์**เป็นชุดเดียวกับในแถบบนเดสก์ท็อป ไม่ใช่สำเนา** — กล่องที่ครอบเป็น `min-[1366px]:contents` -ตั้งแต่ 1366px ขึ้นไปจึงละลายกลับเข้าแถวเหมือนเดิม ระหว่าง 1024–1365px เป็นแผงที่หล่นลงมาใต้แถบ ([แท็บเล็ต](#-แท็บเล็ตและมือถือจอพับ)) +ตั้งแต่ 1366px ขึ้นไปจึงละลายกลับเข้าแถวเหมือนเดิม ระหว่าง 1024–1365px เป็นแผงที่หล่นลงมาใต้แถบ ([แท็บเล็ต](#แท็บเล็ตและมือถือจอพับ)) บนมือถือกลายเป็นแผ่น ชื่อที่ screen reader กับด่านทดสอบอ่านจึงมีชุดเดียว ด่าน `check:a11y` เปิดทั้งสองแผ่นขึ้นมาสแกนที่ 390px และมี e2e หนึ่ง flow ที่ถามตรง ๆ ว่า "ที่ 390px ทางไปข้อมูลสดอยู่บนจอและมีชื่อไหม" @@ -1918,7 +1903,7 @@ e2e หนึ่ง flow ที่ 390px ด้วยจอสัมผัสเ **ยังไม่รองรับ:** เมนูคลิกขวาที่หัวแถว/คอลัมน์ต้องกดค้าง ซึ่งบางเบราว์เซอร์บนมือถือจะเด้งเมนูของตัวเองมาแทน -### 👆 คัดลอก วาง เติมลงด้วยนิ้ว +### คัดลอก วาง เติมลงด้วยนิ้ว บนเมาส์ สิ่งเหล่านี้คือ `Ctrl+C` `Ctrl+X` `Ctrl+V` `Ctrl+D` และ `Delete` บนมือถือไม่มีทั้งคีย์บอร์ดให้กดและไม่มีคลิกขวา ในการทดสอบแบบ blind งาน "ก๊อปคำว่า ใช่ ลงไปสิบแถว" **ทำไม่ได้เลย** ตอนนี้มีแถวคำสั่งห้าปุ่มที่มีชื่อ — @@ -1933,7 +1918,7 @@ e2e หนึ่ง flow ที่ 390px ด้วยจอสัมผัสเ **ยังไม่รองรับ:** วางจากคลิปบอร์ดของระบบ (ข้อความที่ก๊อปมาจากแอปอื่น) ผ่านแถบนี้ — เบราว์เซอร์บนมือถือขออนุญาตทุกครั้ง จึงยังใช้คลิปบอร์ดของแอปอย่างเดียว -### 🎯 ลากเลือกช่วงด้วยนิ้วโดยข้อมูลไม่หลุดจอ +### ลากเลือกช่วงด้วยนิ้วโดยข้อมูลไม่หลุดจอ ในการทดสอบแบบ blind รอบ 2 บนมือถือ แทบทุกวิธีเลือกช่วง**เลยไปถึงคอลัมน์ J** และจอเลื่อนไปอยู่ที่ H–J จนข้อมูลหายจากจอ ต้องปัดกลับทีละคอลัมน์ห้าถึงเจ็ดครั้ง และกฎจัดรูปแบบที่ทำไว้ก็ไปครอบช่วงที่กว้างเกิน (#127) ตอนนี้: @@ -1951,10 +1936,10 @@ e2e หนึ่ง flow ที่ 390px ด้วยจอสัมผัสเ

มือถือ ช่วง B2:B9 เลื่อนหลุดจอไปทางซ้าย และปุ่มสีน้ำเงิน กลับไปที่ B2:B9 ที่ขอบซ้าย

**`=` แล้วแตะเซลล์อื่นไม่บันทึกทับอีกแล้ว (#99, sev:critical)** — เคย**บันทึก `=` ทับเซลล์ที่กำลังแก้** บนชีตตัวอย่างมันเขียนทับคำว่า -"Mains" ที่มีอยู่ บนจอสัมผัสตอนนี้การแตะคือ[การใส่ที่อยู่เซลล์ลงในสูตร](#-แตะเซลล์ใส่ลงสูตรบนมือถือ) · ด้วยเมาส์ ขณะที่สูตรยังรอที่อยู่เซลล์ (`=`, `=SUM(`, `=A1+`) +"Mains" ที่มีอยู่ บนจอสัมผัสตอนนี้การแตะคือ[การใส่ที่อยู่เซลล์ลงในสูตร](#แตะเซลล์ใส่ลงสูตรบนมือถือ) · ด้วยเมาส์ ขณะที่สูตรยังรอที่อยู่เซลล์ (`=`, `=SUM(`, `=A1+`) การคลิกบนตาราง**ไม่ทำอะไรเลย** ส่วนสูตรที่เขียนจบแล้ว เช่น `=SUM(A1:A3)` คลิกเซลล์อื่นยังบันทึกแล้วย้ายเหมือนเดิม -### 👉 แตะเซลล์ใส่ลงสูตรบนมือถือ +### แตะเซลล์ใส่ลงสูตรบนมือถือ บนมือถือเคยเขียนสูตรได้ทางเดียวคือพิมพ์ที่อยู่เซลล์เองทีละตัว และคีย์บอร์ดมือถือซ่อนวงเล็บกับ `:` ไว้ลึกสองชั้น ตอนนี้ระหว่างที่ช่องแก้ของเซลล์หรือแถบสูตรมีสูตร (ขึ้นต้นด้วย `=`): @@ -1986,7 +1971,7 @@ e2e หนึ่ง flow ที่ 390px ด้วยจอสัมผัสเ **ยังไม่รองรับ:** ด้วยเมาส์ คลิกเซลล์ขณะพิมพ์สูตรยังไม่ใส่ที่อยู่ (Excel ทำเมื่อเคอร์เซอร์อยู่หลังเครื่องหมาย — เป็นงานต่อ) ตอนนี้คลิกแล้วไม่ทำอะไร เพื่อไม่ให้บันทึกทับ · ลากจุดสีฟ้าของเซลล์ที่ชี้ยังขยายได้ทางเดียวจากเซลล์แรกที่แตะ · ยังไม่ได้ลองบน iPhone จริง (e2e ขับผ่าน Chromium) -### 📲 แท็บเล็ตและมือถือจอพับ +### แท็บเล็ตและมือถือจอพับ บน Galaxy Z Fold7 ตอนกางจอ แถบเครื่องมือด้านบน**เลื่อนหลุดขอบจอ** — ความกว้างแบบนั้นเกินมือถือแต่ยังไม่ถึงโน้ตบุ๊ก และเลย์เอาต์เดิมมีแค่สองแบบ: ต่ำกว่า 640px กับที่เหลือทั้งหมด ที่ 1024px แถวบนล้นไป 251px และสิ่งที่ถูกดันหลุดขอบคือปุ่ม "ถาม AI" @@ -1994,7 +1979,7 @@ e2e หนึ่ง flow ที่ 390px ด้วยจอสัมผัสเ | ความกว้าง | หน้าตา | |---|---| -| **ต่ำกว่า 1024px** (มือถือ, Fold ตอนกาง, แท็บเล็ตแนวตั้ง) | แบบ[มือถือ](#-ใช้บนมือถือได้): แถบล่างมีชื่อ เมนูเป็นแผ่น เครื่องมือเซลล์เป็นแผ่น | +| **ต่ำกว่า 1024px** (มือถือ, Fold ตอนกาง, แท็บเล็ตแนวตั้ง) | แบบ[มือถือ](#ใช้บนมือถือได้): แถบล่างมีชื่อ เมนูเป็นแผ่น เครื่องมือเซลล์เป็นแผ่น | | **1024–1365px** (แท็บเล็ตแนวนอน, โน้ตบุ๊กเล็ก) | ปุ่มพาเนลที่มีชื่ออยู่บนแถบบน · **เมนูหล่นเป็นแผงใต้ปุ่ม** · แถบรูปแบบเหลือ B I U จัดตำแหน่ง สี แล้ว**เครื่องมือ**เปิดเป็นแผงใต้แถบ · ตั้งแต่ **1280px** ปุ่ม **นำเข้าไฟล์** กับ **ส่งออก Excel** กลับมาอยู่บนแถบ | | **ตั้งแต่ 1366px** | ทุกอย่างอยู่ในแถว เหมือนเดิม | @@ -2004,7 +1989,7 @@ e2e หนึ่ง flow ที่ 390px ด้วยจอสัมผัสเ และด่าน `check:a11y` เช็กการเลื่อนแนวนอนที่ห้าความกว้างทุก push · ตอนแรกปุ่มไฟล์ทั้งสี่ถูกย้ายเข้าเมนูที่ต่ำกว่า 1366px ทั้งหมด แล้ว e2e ก็แดง — ที่ 1280px ซึ่งเป็นความกว้างโน้ตบุ๊กที่พบบ่อยที่สุด การส่งออกจะต้องกดสองครั้งทุกครั้ง พื้นที่ยังเหลือพอสำหรับสองปุ่มที่ใช้บ่อย จึงเอาสองปุ่มนั้นกลับมา -### 📐 ข้อความทีละอัน และจอเตี้ย +### ข้อความทีละอัน และจอเตี้ย บนมือถือหลังเปิดไฟล์ แถบเตือน "บันทึกในเบราว์เซอร์นี้เท่านั้น" (สามบรรทัด) กับแถบ "เปิดไฟล์แล้ว · ย้อนกลับ" ซ้อนกันราว 170px เหลือตาราง **10 แถว** ที่ 390×844 · มือถือแนวนอนกับจอที่ซูม 200% **ไม่เหลือแถวเลย** เพราะแถบหกชั้นกินจอหมด (#129) @@ -2012,7 +1997,7 @@ e2e หนึ่ง flow ที่ 390px ด้วยจอสัมผัสเ - **ข้อความเหนือตารางขึ้นทีละอัน** เรียงตามความสำคัญ: แท็บนี้ดูอย่างเดียว (#47) › บันทึกไม่ลง › pivot ที่ข้อมูลต้นทางเปลี่ยน (ตัวเลขบนจอไม่ตรงสำคัญกว่าคำแนะนำ) › ข้อมูลตัวอย่าง › บันทึกในเบราว์เซอร์นี้ - **ข้อความหลังเปิดไฟล์ลอยเหนือท้ายตาราง** ไม่ดันตารางลง หายเองใน 10 วินาทีหรือเมื่อแก้ต่อ เฉพาะปุ่มของมันที่รับการแตะ ปัดข้างที่เริ่มบนตัวหนังสือของมันจึงเลื่อนตารางข้างใต้ได้ -- **"บันทึกในเบราว์เซอร์นี้เท่านั้น"** เหลือบรรทัดเดียวในรอบที่แก้ครั้งแรก แล้วย้ายไป[อยู่หลังสถานะบันทึก](#-บันทึกอัตโนมัติ--undoredo) +- **"บันทึกในเบราว์เซอร์นี้เท่านั้น"** เหลือบรรทัดเดียวในรอบที่แก้ครั้งแรก แล้วย้ายไป[อยู่หลังสถานะบันทึก](#บันทึกอัตโนมัติ--undoredo) - **จอเตี้ยกว่า 500px** (มือถือแนวนอน, ซูม 200%) แถบพับเข้าหากัน: - แถบบนกับแถบสูตรอยู่แถวเดียวกัน (ตั้งแต่กว้าง 640px) - แถบรูปแบบพับไว้ กดปุ่มแปรง **"รูปแบบเซลล์"** บนแถบสูตรเพื่อเปิด (ชื่อเดียวกันทั้ง tooltip และที่ screen reader อ่าน พร้อมบอกว่าเปิดหรือพับอยู่) @@ -2029,7 +2014,7 @@ e2e หนึ่ง flow ที่ 390px ด้วยจอสัมผัสเ **ยังไม่รองรับ:** ข้อความหลังเปิดไฟล์บังตารางสองสามแถวระหว่างที่ขึ้นอยู่ · ปัดข้างที่เคยค้างในการทดสอบของผู้ใช้ **ทำซ้ำไม่ได้แล้วบน main** (build ที่ทดสอบเก่ากว่า #142) จึงมี e2e คุมไว้แทน -### 🖱️ คลิกขวาที่เซลล์ +### คลิกขวาที่เซลล์ เดิมคลิกขวาที่เซลล์ได้**เมนูของเบราว์เซอร์** (ย้อนกลับ · โหลดใหม่ · ตรวจสอบ) ส่วนเมนูของแอปมีแค่ที่หัวแถว/หัวคอลัมน์ ซึ่งไม่ใช่ที่แรกที่คนจาก Excel มองหา ตอนนี้คลิกขวาที่เซลล์ได้เมนูของแอป: @@ -2043,7 +2028,7 @@ e2e หนึ่ง flow ที่ 390px ด้วยจอสัมผัสเ และ**เปิดจากคีย์บอร์ดได้ด้วย `Shift+F10` หรือปุ่ม Menu** ตรงเซลล์ที่เลือก ไม่ใช่ตรงที่เมาส์อยู่ - ตอนกำลังแก้ข้อความในช่อง คลิกขวายังเป็น**เมนูของเบราว์เซอร์**ตามเดิม เพราะตรงนั้นต้องการตรวจคำสะกดกับวางแบบข้อความ -### 📏 ลากปรับความกว้างคอลัมน์ +### ลากปรับความกว้างคอลัมน์ **ลากขอบขวาของหัวคอลัมน์**เพื่อปรับความกว้าง · **ดับเบิลคลิกที่ขอบ**เพื่อให้กว้างพอดีข้อความที่ยาวที่สุดในคอลัมน์ แบบ Excel ความกว้างเก็บไว้กับชีต รอดการรีโหลด และส่งออกไปใน `.xlsx` · การลากหนึ่งครั้งคือ undo หนึ่งก้าว ไม่ใช่หนึ่งก้าวต่อพิกเซล @@ -2057,7 +2042,7 @@ e2e หนึ่ง flow ที่ 390px ด้วยจอสัมผัสเ **ยังไม่รองรับ:** ปรับความกว้างด้วยคีย์บอร์ด และปรับความสูงแถวด้วยการลาก (ความสูงจากไฟล์ที่นำเข้ายังแสดงครบ) -### ➕ แทรก/ลบแถว-คอลัมน์ +### แทรก/ลบแถว-คอลัมน์ คลิกขวาที่หัวแถว/หัวคอลัมน์เพื่อแทรกหรือลบ ระบบ**ปรับสูตรทุกเซลล์ในตารางให้อ้างอิงตำแหน่งใหม่ถูกต้องอัตโนมัติ** สูตรที่อ้างอิงถึงแถว/คอลัมน์ที่ถูกลบไปพอดีจะกลายเป็น `#REF!` ให้เห็นชัดเจนเหมือน Excel @@ -2065,7 +2050,7 @@ e2e หนึ่ง flow ที่ 390px ด้วยจอสัมผัสเ

แทรกแถวที่ 3 แล้ว =SUM(C2:C4) กลายเป็น =SUM(C2:C5) เอง — ไม่ต้องไล่แก้สูตรเอง

-### 🔗 รวมเซลล์ (merge) +### รวมเซลล์ (merge) หัวรายงานที่พาดยาวคลุมทั้งตารางคือเซลล์ที่ถูก merge เกือบทุกครั้ง เดิมแอปนี้**อ่าน**เซลล์ที่ merge จากไฟล์ ที่นำเข้ามาได้ และส่งออกกลับไปได้ แต่สร้างเองไม่ได้ ตอนนี้เลือกช่วงแล้วกด **รวมเซลล์** ในแถบรูปแบบ @@ -2098,7 +2083,7 @@ merge เลื่อนตามการแทรก/ลบแถวและ **ยังไม่รองรับ:** จัดข้อความให้อยู่กลางเซลล์ที่ merge ในแนวตั้ง · และการเลือกเซลล์ที่ merge แล้วลากขยายช่วง ยังนับจากมุมซ้ายบนของมัน -### 🔤 เรียงลำดับและกรองข้อมูล +### เรียงลำดับและกรองข้อมูล - **เรียงลำดับ** (A-Z/Z-A): เลือกเซลล์เดียวก็เดาขอบเขตตารางให้อัตโนมัติ และ**หัวตารางอยู่แถวบนเสมอ ไม่ว่าเรียงทางไหน** (#49) · แถวบนสุดนับเป็นหัวตารางเมื่อไม่มีตัวเลขอยู่ในนั้น และมีอย่างใดอย่างหนึ่งบอกว่ามันต่างจากแถวข้างล่าง: คอลัมน์*ไหนก็ได้* @@ -2123,7 +2108,7 @@ merge เลื่อนตามการแทรก/ลบแถวและ จึงถูกเรียงไปด้วย — เลือกเฉพาะแถวที่จะเรียงก่อนกดถ้าต้องการให้แน่นอน (Excel ก็เดาแบบเดียวกัน) · ตัวหนา/สี/รูปแบบตัวเลข ยังใส่ให้แถวที่ซ่อนด้วย ไม่มีค่าไหนหาย แต่รูปแบบเปลี่ยน -### 🧮 สรุปข้อมูล (Pivot) +### สรุปข้อมูล (Pivot) เลือกช่วงที่มีหัวตารางอยู่แถวแรก กด **"สรุปข้อมูล (Pivot)"** แล้วเลือกว่าจะจัดกลุ่มด้วยคอลัมน์ไหน (เลือกได้หลายคอลัมน์) จะแตกเป็นคอลัมน์ด้วยอะไร และจะสรุปค่าอะไรด้วยวิธีไหน — ผลรวม / จำนวน / ค่าเฉลี่ย / @@ -2170,7 +2155,7 @@ merge เลื่อนตามการแทรก/ลบแถวและ **ยังไม่รองรับ:** ต้องกดสรุปใหม่เอง ไม่ได้รีเฟรชอัตโนมัติ · การกดสรุปใหม่เขียนทับสิ่งที่พิมพ์เพิ่มลงในชีตสรุป · แตกเป็นคอลัมน์ได้ทีละหนึ่งฟิลด์ · ยังไม่มีตัวกรองของตารางสรุปเอง -### 📑 หลายชีตในไฟล์เดียว +### หลายชีตในไฟล์เดียว สลับ/เพิ่ม/เปลี่ยนชื่อ/ลบชีตจากแถบด้านล่างตาราง แต่ละชีตมีข้อมูล สูตร และรูปแบบแยกอิสระ แต่ **Undo/Redo และ บันทึกอัตโนมัติครอบคลุมทุกชีตพร้อมกัน** @@ -2189,12 +2174,12 @@ merge เลื่อนตามการแทรก/ลบแถวและ - **สิ่งที่เปลี่ยนไปและตั้งใจให้เป็นแบบนี้:** ถ้าเคยมีชื่อซ้ำ สูตรเคยอ่านแท็บ*หลัง* ตอนนี้อ่านแท็บแรก ค่าในสูตรพวกนั้นจึงเปลี่ยน — จากค่าของแท็บที่ถูกสร้างทีหลัง กลับเป็นค่าของแท็บที่สูตรตั้งใจอ่าน -### 📥 นำเข้าไฟล์ Excel ที่มีอยู่แล้ว +### นำเข้าไฟล์ Excel ที่มีอยู่แล้ว อ่านค่าทุกเซลล์รวมถึง**สูตรต้นฉบับ** เข้ามาแล้วคำนวณผลลัพธ์ใหม่ทันที รองรับไฟล์หลายชีต (นำเข้าเป็นแท็บแยกให้ครบ) ส่วน**หน้าตาของไฟล์** — แถบสี ขนาดตัวอักษร เส้นขอบ ความสูงแถว เซลล์ที่ merge — อ่านมาครบเช่นกัน -ดู [เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม](#-เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม) และถ้าไฟล์นั้นถูกทำเป็นฟอร์มไว้ -ดู [แม่แบบ (Template) จากไฟล์ Excel](#-แม่แบบ-template-จากไฟล์-excel) +ดู [เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม](#เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม) และถ้าไฟล์นั้นถูกทำเป็นฟอร์มไว้ +ดู [แม่แบบ (Template) จากไฟล์ Excel](#แม่แบบ-template-จากไฟล์-excel) **สูตรที่ลากลงมาหรือลากไปทางขวาใน Excel ยังเป็นสูตรทุกเซลล์** — Excel เก็บช่วงที่ลากเป็น *shared formula* (เซลล์แรกมีข้อความสูตร เซลล์ที่เหลือแค่ชี้กลับไปหา) แอปเลื่อนการอ้างอิงให้แต่ละเซลล์ด้วยตัวเลื่อนเดียวกับ fill handle @@ -2211,7 +2196,7 @@ merge เลื่อนตามการแทรก/ลบแถวและ > ราว 780 MB · ไฟล์ที่ยาวกว่านั้นจะถูกเปิดเท่าที่เพดานให้ และ**แอปบอกทันที**ว่าชีตไหนยาวถึงแถวเท่าไรและเปิดได้กี่แถว > ไม่ตัดเงียบ -### 🧾 บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้ +### บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้ คนที่ใช้แอปนี้คือคนที่ต้อง **เปิดไฟล์ แก้ แล้วส่งกลับ** โดยไฟล์ไม่เพี้ยน (#83) · ถ้าไฟล์มีสิ่งที่แอปเก็บไว้ไม่ได้ เช่น รูปภาพ หรือกราฟของ Excel การส่งออกแล้วบันทึกทับไฟล์เดิมจะทำให้สิ่งนั้นหายไป เดิมไม่มีอะไรบอก คนที่รู้คือคนที่ได้รับไฟล์ · @@ -2245,7 +2230,7 @@ merge เลื่อนตามการแทรก/ลบแถวและ ([#159](https://github.com/SuruchBoss/ExcelToGo/issues/159)) - รายงานไม่ถูกบันทึกไว้ในเบราว์เซอร์ โหลดหน้าใหม่แล้วเปิดดูอีกไม่ได้ (ไฟล์ต้นฉบับยังบอกได้ทุกครั้งที่เปิด) -### 🛟 เปิดไฟล์ใหม่โดยไม่ทับงานเดิม +### เปิดไฟล์ใหม่โดยไม่ทับงานเดิม เดิม**การนำเข้าไฟล์แทนที่ทั้ง workbook ทันที ไม่ถามสักคำ** — ในการทดสอบแบบ blind มีคนเปิดไฟล์เพื่อดูตัวเลขตัวเดียว แล้วงานที่พิมพ์มาเกือบชั่วโมงหายไป ตอนนี้ถ้ามีงานเปิดอยู่ แอป**ถามก่อน**: @@ -2261,7 +2246,7 @@ merge เลื่อนตามการแทรก/ลบแถวและ ข้อความหายเองใน 10 วินาทีหรือเมื่อแก้อะไรต่อ ส่วนปุ่มย้อนกลับบนแถบบนยังทำแบบเดียวกันได้เสมอ · เดิมเป็นแถบที่ดันตารางลง (#129) **ไม่ถาม** ถ้าบนจอเป็นแค่ข้อมูลตัวอย่างหรือชีตว่าง — คำถามที่ไม่มีอะไรจะเสียคือคำถามที่สอนให้คนกดผ่านโดยไม่อ่าน -### 🆕 เริ่มจากตารางว่าง · ไฟล์ใหม่ +### เริ่มจากตารางว่าง · ไฟล์ใหม่

เปิดแอปครั้งแรก: ตารางว่าง และแถบเขียวที่มีปุ่มลองกับข้อมูลตัวอย่าง

@@ -2283,7 +2268,7 @@ merge เลื่อนตามการแทรก/ลบแถวและ ภาษาอังกฤษ แถวบนสุดเหลือที่ว่างแค่ 7px · แถวยังไม่ถูกวาดต่อจากแถวสุดท้ายแบบคอลัมน์ ชีตใหม่มี 30 แถว เพิ่มด้วยปุ่ม "+ แถว" · เทมเพลตที่ล็อกโครงไว้ไม่วาดคอลัมน์ต่อ เพราะขยายไม่ได้ · ย้อนกลับได้เฉพาะในแท็บนี้ ปิดแท็บแล้วงานที่ถูกแทนที่ไม่กลับมา จึงมีปุ่มส่งออกให้ก่อน -### 🧩 ใส่สูตรแบบลากวาง +### ใส่สูตรแบบลากวาง ค้นหา/กรองตามหมวดหมู่ (คณิตศาสตร์ / สถิติ / ตรรกะ / ข้อความ / วันที่ / ค้นหา) แล้ว **ลากวาง** หรือ **คลิก** การ์ดสูตร → เปิดแผงกรอกพารามิเตอร์พร้อมปุ่มเป้าเล็ง (🎯) ให้คลิก/ลากเลือกเซลล์จากตารางแทนการพิมพ์ที่อยู่เอง @@ -2302,7 +2287,7 @@ merge เลื่อนตามการแทรก/ลบแถวและ -### 📤 ส่งออกไฟล์ +### ส่งออกไฟล์ - **Excel**: ไฟล์ `.xlsx` เดียวที่มี**ทุกชีต**ครบ พร้อมสูตรต้นฉบับ, รูปแบบ (แถบสี/ขนาดตัวอักษร/เส้นขอบ/ ความสูงแถว/ความกว้างคอลัมน์/เซลล์ที่ merge) และการล็อกของแม่แบบ — เปิดต่อใน Excel/Google Sheets แล้วยังเป็น @@ -2310,12 +2295,12 @@ merge เลื่อนตามการแทรก/ลบแถวและ ไฟล์จึงไม่ควรสัญญาเกินกว่าที่แอปแสดง · **ทุกสูตรมีค่าผลลัพธ์ติดไปในไฟล์ด้วย รวมสูตรข้ามชีต** (#101) — โปรแกรมที่ไม่คำนวณใหม่ (พรีวิวใน LINE/อีเมล ตัวดูไฟล์บนมือถือ) แสดงค่านี้ตรง ๆ จึงเห็นตัวเลขจริง ไม่ใช่ 0 · มีเทสต์แกะไฟล์ส่งออกแล้วอ่าน `` ใน XML ตรง ๆ - · **ชื่อชีตในไฟล์ตรงกับในแอปเสมอ** เพราะแอปรับเฉพาะชื่อที่ Excel รับ (ดู[หลายชีตในไฟล์เดียว](#-หลายชีตในไฟล์เดียว)) + · **ชื่อชีตในไฟล์ตรงกับในแอปเสมอ** เพราะแอปรับเฉพาะชื่อที่ Excel รับ (ดู[หลายชีตในไฟล์เดียว](#หลายชีตในไฟล์เดียว)) และถ้าส่งออกไม่สำเร็จ แอปบอกทันที ไม่ใช่กดแล้วเงียบ · **ยังไม่รองรับ:** ฟังก์ชันรุ่นใหม่ (`XLOOKUP` `SORT` `FILTER` `UNIQUE` ฯลฯ) ถูกเขียนลงไฟล์โดยไม่มี prefix `_xlfn.` ที่ Excel อาจต้องการ เปิดใน Excel จึงอาจขึ้น `#NAME?` — ยังไม่แก้เพราะยังยืนยันกับเอกสารของ Microsoft ไม่ได้ และไม่อยากเดา ([#156](https://github.com/SuruchBoss/ExcelToGo/issues/156)) -- **CSV**: เฉพาะชีตที่เปิดอยู่ เป็นค่าที่คำนวณแล้ว พร้อม BOM ให้ Excel อ่านไทยออก (ดู [นำเข้า/ส่งออก CSV](#-นำเข้าส่งออก-csv)) +- **CSV**: เฉพาะชีตที่เปิดอยู่ เป็นค่าที่คำนวณแล้ว พร้อม BOM ให้ Excel อ่านไทยออก (ดู [นำเข้า/ส่งออก CSV](#นำเข้าส่งออก-csv)) - **PDF**: เฉพาะชีตที่เปิดอยู่ แสดงค่าที่คำนวณแล้วพร้อมหัวแถว/คอลัมน์ และกราฟต่อท้ายตาราง · **สีพื้นกับสีตัวอักษรของเซลล์ออกไปด้วย** (เดิมทุกช่องขาวหมด ชีตที่ระบายหัวตารางไว้จึงออกมาเหมือนไม่เคยจัดรูปแบบ) แถวหัวตารางที่ระบายสีอ่อนไว้จะได้ตัวอักษรสีเข้มแทนตัวขาวของหัวตาราง PDF · ยังไม่รองรับ: ตัวหนา/ตัวเอียงใน PDF เพราะแต่ละแบบต้องฝังฟอนต์ไทยอีกไฟล์ @@ -2348,7 +2333,7 @@ merge เลื่อนตามการแทรก/ลบแถวและ **ยังไม่มีกล่องตั้งค่าหน้ากระดาษ** และนี่คือจุดที่ตั้งใจหยุด ไม่ใช่การลืม: ค่าเริ่มต้นถูกบ่อยพอจนกล่องนั้น จะกลายเป็นสิ่งที่ต้องกดผ่านเป็นส่วนใหญ่ · ขอบกระดาษ, สเกลที่เลือกเอง และช่วงที่จะพิมพ์ คือสิ่งที่กล่องนั้นจะเพิ่มให้ และยังไม่มี -### 🔀 นำเข้า/ส่งออก CSV +### นำเข้า/ส่งออก CSV CSV คือภาษาที่เครื่องมืออื่นพูดกันหมด — รายการเดินบัญชี, ยอดขายจากเครื่อง POS, ไฟล์ที่เพื่อนร่วมงานส่งมา เดิมต้องเอาไปเปิดใน Excel แล้ว save เป็น `.xlsx` ก่อนถึงจะเปิดที่นี่ได้ ซึ่งทำให้แอปนี้เป็นทางอ้อมของ @@ -2379,7 +2364,7 @@ encoding ให้ · และช่องที่ขึ้นต้นด้ เราเป็นคนใส่ `'` ไว้เอง ตอนอ่านกลับเข้ามาจึงเสีย `'` ตัวนั้นไป — CSV ไม่มีวิธีบอกว่า "ข้อความที่หน้าตา เหมือนสูตร" จึงต้องเลือกอย่างใดอย่างหนึ่ง -### 🎨 เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม +### เปิดไฟล์มาแล้วหน้าตาเหมือนเดิม

@@ -2404,7 +2389,7 @@ encoding ให้ · และช่องที่ขึ้นต้นด้ > **ยังไม่รองรับ:** รูปภาพ/กราฟในชีต, ฟอนต์เฉพาะ (ใช้ฟอนต์ของระบบ) และการกดลูกศร > เข้าไปใน "ช่องที่ถูก merge กลืนไป" (ช่องนั้นไม่มีอยู่ใน DOM แล้ว — คลิกที่แถบ merge ได้ตามปกติ) -### 📋 แม่แบบ (Template) จากไฟล์ Excel +### แม่แบบ (Template) จากไฟล์ Excel

@@ -2438,7 +2423,7 @@ dropdown ยังอยู่ ความกว้างคอลัมน์ > **ยังไม่รองรับ:** การ*สร้าง*แม่แบบขึ้นเองในแอป — ตอนนี้อ่านแม่แบบจากไฟล์ที่ทำมาแล้วเท่านั้น > (เซลล์ที่ merge และ conditional formatting เคยอยู่ในรายการนี้ ตอนนี้ทำเสร็จแล้วทั้งคู่) -### 📊 นับยอดใช้งานโดยพิสูจน์ได้ว่าระบุตัวใครไม่ได้ +### นับยอดใช้งานโดยพิสูจน์ได้ว่าระบุตัวใครไม่ได้ คำถามที่เล็กที่สุดที่มีประโยชน์จริงคือ **"มีคนใช้จริงไหม หรือที่เห็นคือตัวเราเองเข้าไปดู"** และมันขัดกับประโยคที่ หน้าแรกชูไว้ตรง ๆ รูปร่างของมันเลยต้องเป็นตัวคำตอบเอง ไม่ใช่เชิงอรรถ @@ -2508,7 +2493,7 @@ connection string ใส่ `code` แล้ว assert ว่ามันไม กับสองคนเข้าคนละครั้ง เป็นเลขเดียวกัน), มีใครกลับมาอีกไหม, มาจากไหน, และอะไรก็ตามเกี่ยวกับการเข้าครั้งใด ครั้งหนึ่ง -### 🏠 หน้า landing อธิบายแอป +### หน้า landing อธิบายแอป เปิด `localhost:3000` จะเจอหน้าอธิบายว่าแอปทำอะไรได้บ้างก่อน แล้วค่อยกด **"เปิดแอป"** เข้า `/app` — เดิมเปิดมา เจอตารางเปล่าๆ เลยโดยไม่มีอะไรบอกว่าใช้ยังไง ภาพทุกรูปบนหน้านี้ถ่ายจากแอปจริง ไม่ใช่ mockup และใช้ dictionary @@ -2531,7 +2516,7 @@ connection string ใส่ `code` แล้ว assert ว่ามันไม **หัวข้อแรกเลิกเป็นรายการฟีเจอร์ — สองรอบ** · รอบแรก ของเดิมเขียนว่า "จำ syntax ไม่ได้" "พิมพ์ที่อยู่เซลล์ผิด" ซึ่งเป็นรายการความสามารถที่แต่งให้ดูเหมือนปัญหา ไม่มีใครอ่านแล้วคิดว่า *นี่แหละกู* จึงกลายเป็นสถานการณ์ที่เล่าเป็นเรื่อง แต่ปัญหากับฟีเจอร์ยังแยกกันอยู่คนละส่วน ส่วนปัญหาไม่มีภาพ ส่วนฟีเจอร์ไม่บอกว่าแก้ปัญหาอะไร · รอบนี้รวมเป็นส่วนเดียว: -**[หกปัญหาที่ทีมจ่ายอยู่ทุกเดือน](#-ปัญหาที่แก้ได้)** แต่ละข้อเขียนแบบรายการในบัญชี — ใครเจอ · **ต้นทุนตอนนี้เป็นหมึกแดง** +**[หกปัญหาที่ทีมจ่ายอยู่ทุกเดือน](#ปัญหาที่แก้ได้)** แต่ละข้อเขียนแบบรายการในบัญชี — ใครเจอ · **ต้นทุนตอนนี้เป็นหมึกแดง** (สีเดียวบนหน้าที่ใช้กับตัวหนังสือ) · ฟีเจอร์ที่แก้เรียงเป็นบรรทัด a b c เพราะปัญหาหนึ่งข้อมักแก้ด้วยหลายฟีเจอร์ทำงานต่อกัน · แล้ว**ผลลัพธ์อยู่ใต้เส้นคู่** แบบที่บัญชีปิดยอดรวม @@ -2575,7 +2560,7 @@ stack ของ `font-mono` จึงต่อท้ายด้วย Plex Sans อีเมล**พิมพ์ไว้ให้เห็นเต็มๆ** แทนที่จะซ่อนหลังปุ่ม "ติดต่อ" พร้อมปุ่มคัดลอก เพราะคนที่ใช้เว็บเมลกดลิงก์ `mailto:` แล้วไม่มีอะไรเกิดขึ้น ผลการคัดลอกประกาศผ่าน live region ส่วน footer ก็มีลิงก์อีเมลเพิ่มข้าง LinkedIn กับ GitHub -### ❓ คำถามที่พบบ่อยบนหน้าแรก +### คำถามที่พบบ่อยบนหน้าแรก

หัวข้อคำถามที่พบบ่อยบนหน้าแรก เจ็ดคำถาม แต่ละข้อตอบในประโยคแรกแล้วตามด้วยข้อจำกัด

@@ -2591,7 +2576,7 @@ stack ของ `font-mono` จึงต่อท้ายด้วย Plex Sans - ตัวเลขในคำตอบ (สูตรพร้อมใช้ 37, ฟังก์ชัน 66) ถูกนับจากโค้ด — **เทสต์นี้เองที่เจอว่าตัวนับฟังก์ชันขาดไปหนึ่งตัวมาตลอด** (`"RANK.EQ"` มีเครื่องหมายคำพูด regex เดิมไม่นับ) ตัวเลข 65 บนหน้าแรก README และการ์ด link preview จึงเป็น 66 แล้ว -### 📘 หน้าสอนสูตรทีละตัว +### หน้าสอนสูตรทีละตัว

หน้าสูตร SUMIF: ประโยคที่ตอบว่าสูตรทำอะไร รูปแบบของสูตรกับความหมายของแต่ละช่อง และตารางตัวอย่างที่แถวของสาขาเหนือถูกไฮไลต์ ผลรวม 450 คำนวณโดยเอนจินของแอป พร้อมปุ่มลองในตาราง

@@ -2612,19 +2597,19 @@ stack ของ `font-mono` จึงต่อท้ายด้วย Plex Sans ถือ nonce ของ CSP · `/formulas/SUMIF` redirect (308) ไป `/formulas/sumif` - **ข้อจำกัด:** หน้าเหล่านี้เป็นภาษาไทยเท่านั้นจนกว่าจะมี URL ภาษาอังกฤษของตัวเอง · สูตรที่เหลือในแถบสูตรยังมีคำอธิบายอยู่ในแอปเท่านั้น (หน้ารวมนับจำนวนจากแถบสูตรเอง) -### 🌐 สองภาษา (ไทย / English) +### สองภาษา (ไทย / English) กดปุ่ม **EN**/**ไทย** มุมขวาบนเพื่อสลับ UI ทั้งแอปทันที — เมนู ปุ่ม ชื่อ/คำอธิบายสูตรทั้ง 37 ตัว ข้อความแจ้งเตือน และคำตอบจาก AI (ทั้ง heuristic และ Claude จริง) เปลี่ยนตามภาษาที่เลือกเสมอ จำภาษาไว้ในเบราว์เซอร์ · **ครั้งแรกที่เปิด เลือกภาษาตามเบราว์เซอร์** (ภาษาไทยอยู่ในรายการภาษาของเบราว์เซอร์ → ไทย, ไม่อยู่ → อังกฤษ) เดิมทุกคนเจอภาษาไทยก่อน คนอ่านไทยไม่ออกจึงต้องหาปุ่ม EN จากหน้าที่อ่านไม่ออก · `` กับชื่อแท็บเปลี่ยนตามด้วย screen reader จึงอ่านถูกภาษา (ชื่อแท็บเคยค้างเป็นภาษาไทยบนเบราว์เซอร์อังกฤษ เพราะ React เขียนชื่อจาก metadata ทับหลังโหลด — ตอนนี้มีตัวคอยเขียนกลับ QA รอบ 2 เจอ) ดูรายละเอียด -สถาปัตยกรรมที่ [ระบบสองภาษา (i18n)](#-ระบบสองภาษา-i18n) +สถาปัตยกรรมที่ [ระบบสองภาษา (i18n)](#ระบบสองภาษา-i18n)

ชีตตัวอย่างเดิมหลังกด EN ครั้งเดียว: ทุกป้ายเป็นภาษาอังกฤษ

--- -## 🛠 เทคโนโลยีที่ใช้ +## เทคโนโลยีที่ใช้ ### Framework / ภาษา @@ -2651,7 +2636,7 @@ stack ของ `font-mono` จึงต่อท้ายด้วย Plex Sans > **หมายเหตุ:** ไม่ได้ใช้ไลบรารีคำนวณสูตรสำเร็จรูป (เช่น HyperFormula) แต่เขียน **เอนจินคำนวณสูตรขึ้นเอง** > ทั้ง tokenizer, parser, evaluator และฟังก์ชันต่างๆ เพื่อควบคุมพฤติกรรมได้เต็มที่ ดูรายละเอียดที่หัวข้อ -> [เอนจินคำนวณสูตร](#-เอนจินคำนวณสูตร) +> [เอนจินคำนวณสูตร](#เอนจินคำนวณสูตร) ### เครื่องมือ @@ -2662,7 +2647,7 @@ stack ของ `font-mono` จึงต่อท้ายด้วย Plex Sans --- -## 🏛 สถาปัตยกรรม +## สถาปัตยกรรม ### ภาพรวมระบบ @@ -2791,7 +2776,7 @@ formulaEngine/ (tokenizer → parser → evaluator → functions) --- -## 📁 โครงสร้างโปรเจกต์ +## โครงสร้างโปรเจกต์ ``` src/ @@ -3030,7 +3015,7 @@ public/ --- -## 🧮 เอนจินคำนวณสูตร +## เอนจินคำนวณสูตร ส่วนที่ตั้งใจทำมากที่สุดของโปรเจกต์ เพราะเป็นจุดที่ผิดพลาดแล้วผู้ใช้จะไม่รู้ตัว (ตัวเลขผิดเงียบๆ) @@ -3163,7 +3148,7 @@ flowchart LR > **สิบตัวท้ายมาจากการถามโมเดลจริง ไม่ได้ไล่ตามคู่มือ Excel** — `TEXTJOIN` `FIND` `RANK.EQ` `SUMPRODUCT` > `CEILING` `CHAR` (กับเพื่อนร่วมตระกูล `SEARCH` `SUBSTITUTE` `FLOOR` `CODE`) คือฟังก์ชันที่ผู้ช่วย AI > ตอบกลับมาแล้วเอนจินรันไม่ได้ ตอนทดสอบด้วย API key จริง — ดู -> [ผู้ช่วย AI](#-ถาม-ai-หาสูตร) ว่าทำไมถึงรู้เรื่องนี้ +> [ผู้ช่วย AI](#ถาม-ai-หาสูตร) ว่าทำไมถึงรู้เรื่องนี้ **`INDEX` + `MATCH` แทน `VLOOKUP` ได้ และทำสิ่งที่ VLOOKUP ทำไม่ได้** — `VLOOKUP` ค้นได้เฉพาะจากคอลัมน์ซ้ายสุด ของตาราง และผูกกับ "ลำดับคอลัมน์ที่เท่าไร" ซึ่งพังเงียบๆ เมื่อมีคนแทรกคอลัมน์: @@ -3190,7 +3175,7 @@ flowchart LR ทั้งสองช่วงต้องเป็นแถวเดียวหรือคอลัมน์เดียวและยาวเท่ากัน — Excel จะ spill ทั้งแถวออกมาถ้าช่วงคำตอบเป็นสองมิติ `XLOOKUP` ตัวนี้ยังไม่ทำ จึงปฏิเสธไปตรงๆ ดีกว่าคืนเซลล์แรกมาแล้วทำเป็นว่าเหมือนกัน (บรรทัดนี้เคยเขียนว่า "เอนจินนี้ไม่มี spill" ซึ่งจริงตอนเขียน และค้างอยู่ต่อหลังจาก -[สูตรที่ตอบเป็นตาราง](#-สูตรที่ตอบมาเป็นทั้งตาราง-array-formula) ทำเสร็จ — spill มีแล้ว แต่ `XLOOKUP` ยังไม่ได้ต่อเข้ากับมัน) +[สูตรที่ตอบเป็นตาราง](#สูตรที่ตอบมาเป็นทั้งตาราง-array-formula) ทำเสร็จ — spill มีแล้ว แต่ `XLOOKUP` ยังไม่ได้ต่อเข้ากับมัน) **เว้นอาร์กิวเมนต์กลางสูตรได้** — `XLOOKUP(a,b,c,,-1)` คือการข้าม `if_not_found` ไปใส่โหมดค้นหา แบบเดียวกับที่ Excel เขียน ก่อนหน้านี้ parser ไม่รับช่องว่างนี้ ทำให้อาร์กิวเมนต์ที่อยู่หลังตัวที่ไม่บังคับใช้ไม่ได้เลยถ้าไม่พิมพ์ค่าที่ไม่ได้ตั้งใจ @@ -3286,7 +3271,7 @@ error อยู่แล้ว คือ `IF` `IFERROR` `IFNA` `COUNT` `COUNTA` --- -## 🌐 ระบบสองภาษา (i18n) +## ระบบสองภาษา (i18n) ไม่ได้ใช้ไลบรารี i18n สำเร็จรูป (เช่น next-intl) เพราะแอปนี้ render ฝั่ง client ทั้งหมดอยู่แล้วและไม่ต้องการ routing ตาม locale (`/en/...`) — เขียน dictionary ของแอปเองแทน โดยให้ **TypeScript เป็นตัวคุมคุณภาพการแปล**: @@ -3320,7 +3305,7 @@ flowchart LR --- -## 🔐 ความปลอดภัย — ทดสอบอะไรไปบ้าง +## ความปลอดภัย — ทดสอบอะไรไปบ้าง ส่วนนี้เขียนไว้เพราะเป็นเส้นแบ่งระหว่าง "สั่ง AI สร้างแอป" กับ "ทำซอฟต์แวร์" — โค้ดที่ดูดีและรันได้ ไม่ได้แปลว่าปลอดภัย และวิธีเดียวที่จะรู้คือลองเจาะมันจริง ๆ @@ -3512,11 +3497,11 @@ B อ่านและแก้ได้ แต่ยังยึดเป็ กับ `@SUM(1+1)*cmd|'/c calc'!A0` ส่งออกออกมาทั้งคู่แบบยิงได้จริง ส่วน `=1+1` ไม่รอด เพราะเอนจินของ แอปนี้คำนวณมันไปแล้ว — **ตัวที่อันตรายคือสามตัวที่เอนจินไม่ถือว่าเป็นสูตรพอดี** "เราคำนวณสูตรเองอยู่แล้ว" จึงไม่เคยเป็นเกราะ ตอนนี้มีเทสต์ความปลอดภัยของตัวเองอยู่ที่ `csvInjection.test.ts` - (ดู [นำเข้า/ส่งออก CSV](#-นำเข้าส่งออก-csv)) + (ดู [นำเข้า/ส่งออก CSV](#นำเข้าส่งออก-csv)) - **ไม่มีระบบผู้ใช้** จึงไม่มีเรื่องสิทธิ์ระดับผู้ใช้ให้ทดสอบ — `SOURCES_ADMIN_TOKEN` เป็นสวิตช์ของ ผู้ดูแลเครื่อง ไม่ใช่บัญชีผู้ใช้ -## 📈 Lighthouse +## Lighthouse วัดจาก production build บนเครื่อง (`npm run build && npm run start`) ด้วย Lighthouse 12 พรีเซ็ต **mobile** ซึ่งจำลอง CPU ช้าลง 4 เท่าและเน็ต slow 4G — ไม่ใช่เลขที่วัดบนเดสก์ท็อปแล้วโชว์ @@ -3534,7 +3519,7 @@ B อ่านและแก้ได้ แต่ยังยึดเป็ มองไม่เห็นเลยเมื่อเทียบกับ LCP สามวินาทีครึ่ง — ตัวเลขใน README ที่จริงเฉพาะกับ build เก่า เป็นความผิดพลาดที่โปรเจกต์นี้เจอมาแล้วสองครั้ง รันใหม่จึงถูกกว่าการมานั่งเถียง -**Accessibility 100 ทั้งสองหน้า** สอดคล้องกับด่าน [`check:a11y`](#-การทดสอบ) ที่รัน axe ทุก PR — +**Accessibility 100 ทั้งสองหน้า** สอดคล้องกับด่าน [`check:a11y`](#การทดสอบ) ที่รัน axe ทุก PR — คนละเครื่องมือ ผลตรงกัน **และทั้งคู่ก็ตัดสินตารางผิดเหมือนกัน ซึ่งควรเขียนไว้** Lighthouse ได้ 100 และ axe ไม่มี violation @@ -3553,9 +3538,9 @@ B อ่านและแก้ได้ แต่ยังยึดเป็ ของ framework เอง ซึ่งแลกไม่คุ้ม — เขียนไว้ตรงนี้แทนที่จะปัดตัวเลขขึ้น > เลขชุดนี้วัดจาก localhost ไม่ใช่จาก Vercel — บนของจริงที่มี CDN และ compression น่าจะดีกว่านี้ -> แต่ผมยืนยันเองไม่ได้ จึงรายงานเฉพาะที่วัดมาจริง +> แต่ยังไม่ได้วัดยืนยัน จึงรายงานเฉพาะค่าที่วัดได้จริง -## 🧪 การทดสอบ +## การทดสอบ ```bash npm test # 2017 เคส ใน 145 ไฟล์ ด้วย Vitest @@ -3664,10 +3649,10 @@ seed ถูกปักไว้ ด่านที่แดงจึงแด ในตารางแล้วสองข้อในflow ที่สามตกทันที (ครั้งแรกที่ลองปิดยังเขียวอยู่ เพราะ `case` ที่แทรกเข้าไปไปอยู่หลัง `case "ArrowRight"` เดิม กลายเป็นโค้ดตาย — การพิสูจน์ต้องเช็กด้วยว่าของที่ตั้งใจทำให้พังนั้นพังจริง) -> **2017 เคสผ่านหมด แต่ 43% ของคำตอบจากผู้ช่วย AI ใช้ไม่ได้** — เพราะเทสต์ส่วนนั้น mock ตัวโมเดลไว้ -> มันจึงตอบตามที่คนเขียนเทสต์คิดว่ามันจะตอบ จำนวนเทสต์บอกได้แค่ว่าเราถามอะไรไปบ้าง ไม่ได้บอกว่าเราถาม -> ครบหรือยัง เรื่องนี้เจอได้ก็ต่อเมื่อเอา API key จริงมายิง — ดู [สิ่งที่เทสต์ 2017 เคสจับไม่ได้](#-สิ่งที่เทสต์-2017-เคสจับไม่ได้) -> และรันซ้ำเองได้ด้วย `npm run check:ai` +> **เทสต์ 2017 เคสผ่านทั้งหมด แต่คำตอบจากผู้ช่วย AI ใช้งานไม่ได้ถึง 43%** เนื่องจากเทสต์ส่วนนั้น mock ตัวโมเดลไว้ +> จึงตอบตามที่ผู้เขียนเทสต์คาดไว้ จำนวนเทสต์บอกได้เพียงว่าทดสอบอะไรไปบ้าง ไม่ได้บอกว่าทดสอบครบแล้ว +> ปัญหานี้พบได้เมื่อทดลองด้วย API key จริงเท่านั้น ดู[ข้อจำกัดของการทดสอบอัตโนมัติ](#ข้อจำกัดของการทดสอบอัตโนมัติ) +> และทำซ้ำได้ด้วย `npm run check:ai` | ไฟล์ | เคส | ทดสอบอะไร | |---|---|---| @@ -3782,19 +3767,48 @@ CI: `npm run verify` รวมทุกอย่างไว้แล้ว — **บน Vercel:** Vercel อ่าน `engines` แล้วเลือก major ที่ใหม่ที่สุดที่เข้าช่วง จึงได้ **24.x** และเพราะช่วงนี้ไม่ครอบรุ่น ถัดไป Vercel จึงไม่อัปเกรด major ให้เองเงียบ ๆ — รุ่นใหม่ต้องเพิ่มเข้า CI และ `engines` พร้อมกัน +### ข้อจำกัดของการทดสอบอัตโนมัติ + +เทสต์ของผู้ช่วย AI ใช้ mock แทนโมเดลทั้งหมด จึงตอบตามที่ผู้เขียนเทสต์คาดไว้ เมื่อทดลองด้วย API key จริงกับคำถามทั่วไป +14 ข้อ มี **6 คำตอบที่ใช้ฟังก์ชันที่เอนจินนี้ไม่รองรับ** (`TEXTJOIN` `FIND` `RANK.EQ` `SUMPRODUCT` `CEILING` `CHAR`) +ทุกสูตรถูกต้องตามไวยากรณ์ Excel แต่แสดง `#NAME?` ในเซลล์หลังผู้ใช้กดปุ่ม "ใส่ลงตาราง" + +การแก้ไขครั้งแรกทำให้ปัญหารุนแรงขึ้น: system prompt สั่งให้โมเดลตอบ "สูตรที่ใกล้เคียงที่สุดเท่าที่มีในรายการ" +คำถาม _"ต่อชื่อทั้งหมดเป็นบรรทัดเดียว"_ จึงได้ `=SUM(A2:A20)` ซึ่งให้ผล `0` โดยไม่มีข้อความเตือน +**error ที่มองเห็นได้ถูกแทนด้วยตัวเลขผิดที่มองไม่เห็น** + +ปัญหาลักษณะเดียวกันพบอีกครั้งในส่วนอื่นของแอป ขณะที่ทุกด่านผ่าน (เทสต์ทั้งชุดผ่าน และ `axe` ไม่พบ violation ทั้งสองหน้า +ที่สองความกว้าง) การทดลองใช้ build สาธารณะในฐานะผู้ใช้ครั้งแรกราวหนึ่งชั่วโมงพบปัญหาสามข้อที่ไม่มีด่านใดตรวจพบ + +- เมื่อเริ่มใส่สูตรจาก palette แล้วยังไม่เสร็จ **ปุ่มแถบข้างทั้ง 6 ปุ่มไม่ตอบสนอง** เพราะเงื่อนไขการเรนเดอร์ + `pending ? … : mode` ให้ `pending` มาก่อน mode ที่ตั้งไว้ใน store และปุ่ม Escape ไม่ยกเลิกสถานะนั้น + คำสั่งที่กดไว้จึงมีผลภายหลัง พร้อมกับคลิกที่ยกเลิกสูตร +- ตัวจับคำสำคัญ ซึ่งบนเว็บสาธารณะที่ไม่ได้ตั้ง key เป็นคำตอบเดียวที่ผู้ใช้เห็น ตอบ _"รวมยอดขายทั้งหมด"_ ด้วย + `=SUM(E2)` ซึ่งรวมเพียงเซลล์เดียว และตอบ _"ต่อชื่อสินค้ากับหมวดหมู่เข้าด้วยกัน"_ ด้วยสูตรเดียวกัน + ทั้งที่ system prompt ห้ามโมเดลตอบแทนในลักษณะนี้ตั้งแต่การแก้ไขข้างต้น แต่กฎนั้นไม่ได้ครอบคลุมตัวจับคำสำคัญ +- `axe` ผ่านตารางทั้ง 4 กรณี (สองหน้า × สองความกว้าง) แต่สำหรับ screen reader ตารางนั้นเป็นตารางธรรมดาที่ควบคุมไม่ได้ + ไม่มี `role="grid"` ไม่มี `aria-selected` หัวตารางไม่มี `scope` ทุกเซลล์รับ Tab และ + **โฟกัสของเบราว์เซอร์ยังอยู่ที่ A1 ขณะที่ Ctrl+↓ ย้ายเคอร์เซอร์ไป A10** ผลของ `axe` ถูกต้องแล้ว + เพราะ `` ที่มี `` ใน DOM แค่ 41 ตัว ยังเหลือ: การแก้เซลล์ที่ running total ทั้งคอลัมน์อ่านอยู่ยังกินราว 1,000–1,200 ms (fan-out จริง ไม่ใช่แคชพลาด) และยังไม่ได้ทำ virtualization แนวคอลัมน์ -- [x] **คีย์บอร์ดให้ครบแบบ Excel** — ทำแล้ว (ดูหัวข้อ [คีย์บอร์ดแบบเดียวกับ Excel](#️-คีย์บอร์ดแบบเดียวกับ-excel)): +- [x] **คีย์บอร์ดให้ครบแบบ Excel** — ทำแล้ว (ดูหัวข้อ [คีย์บอร์ดแบบเดียวกับ Excel](#คีย์บอร์ดแบบเดียวกับ-excel)): `Ctrl+ลูกศร` สุดขอบข้อมูล, `Shift+ลูกศร` ลากช่วง, `Ctrl+Shift+ลูกศร` ทำทั้งสองอย่าง, `Home`/`End`/`Ctrl+Home`/`Ctrl+End`, `PageUp`/`PageDown` วัดเป็นพิกเซล, `Ctrl+A` สองจังหวะ และหน้าจอวิ่งตามเคอร์เซอร์ทั้งสองแกน **`Ctrl+Space`/`Shift+Space` เลือกทั้งคอลัมน์/แถว และ `Ctrl+Enter` เติมทั้งช่วงจากช่องที่เคอร์เซอร์อยู่ ทำแล้วทั้งสามตัว** (อ้างอิงในสูตรขยับตามเหมือนลากเติม ไม่งั้นจะได้คอลัมน์ที่เป็นเลขผิดเหมือนกันหมด) **ตรึงแถว/คอลัมน์ทำแล้ว** — จุดตรึงอยู่บนตัวชีต จึงรอดการรีโหลด เข้า undo ขยับตามเมื่อแทรกแถวข้างบน และไป-กลับกับ `.xlsx` ได้ -- [x] **ตรึงแถว/คอลัมน์ (freeze panes)** — ทำแล้ว (ดู[คีย์บอร์ดแบบเดียวกับ Excel](#️-คีย์บอร์ดแบบเดียวกับ-excel)): จุดตรึงอยู่บนตัวชีต +- [x] **ตรึงแถว/คอลัมน์ (freeze panes)** — ทำแล้ว (ดู[คีย์บอร์ดแบบเดียวกับ Excel](#คีย์บอร์ดแบบเดียวกับ-excel)): จุดตรึงอยู่บนตัวชีต จึงรอดการรีโหลด เข้า undo ขยับตามเมื่อแทรกแถวข้างบน และไป-กลับกับ `.xlsx` ได้ ยังเหลือ: ตรึงได้เฉพาะที่ตำแหน่ง เคอร์เซอร์ ไม่ใช่ที่จุดเลื่อนใดก็ได้ ซึ่งเป็นรูปแบบเดียวกับปุ่มของ Excel เอง -- [x] **จำกัดสิ่งที่กรอกได้ (data validation)** — ทำแล้ว (ดู[จำกัดสิ่งที่กรอกได้](#-จำกัดสิ่งที่กรอกได้-data-validation)): +- [x] **จำกัดสิ่งที่กรอกได้ (data validation)** — ทำแล้ว (ดู[จำกัดสิ่งที่กรอกได้](#จำกัดสิ่งที่กรอกได้-data-validation)): เลือกจากรายการ/ตัวเลขในช่วง/ความยาว ปฏิเสธก่อนบันทึกและพูดออกมา เลื่อนตามแถว และไป-กลับกับ `.xlsx` ยังเหลือ: "วันที่ในช่วง" กับสูตรกำหนดเอง และรายการที่มีจุลภาคยังเขียนลงไฟล์ไม่ได้ -- [x] **ตั้งชื่อช่วงข้อมูล (named ranges)** — ทำแล้ว (ดู[ตั้งชื่อช่วงข้อมูล](#-ตั้งชื่อช่วงข้อมูล-named-ranges)): +- [x] **ตั้งชื่อช่วงข้อมูล (named ranges)** — ทำแล้ว (ดู[ตั้งชื่อช่วงข้อมูล](#ตั้งชื่อช่วงข้อมูล-named-ranges)): ชื่อภาษาไทยใช้ได้ แทนที่ตอนคอมไพล์จึงไม่ทำให้กราฟ dependency เพี้ยน เลื่อนตามแถว และไป-กลับกับ `.xlsx` และตั้งแต่ #60 ชื่อเป็นของทั้งไฟล์แบบ Excel พร้อมชื่อระดับชีตจากไฟล์ · ยังเหลือ: เลือกขอบเขตตอนตั้งชื่อในแอป · ชื่อที่เป็นสูตร/ทั้งคอลัมน์ · ยังไม่มีช่องชื่อ (name box) ข้างแถบสูตรสำหรับกระโดดไปยังช่วง -- [x] **แหล่งข้อมูลแบบฐานข้อมูล (Postgres/MySQL)** — ทำแล้ว (ดู[ต่อฐานข้อมูลตรง ๆ](#-ต่อฐานข้อมูลตรง-ๆ-postgresql--mysql)): +- [x] **แหล่งข้อมูลแบบฐานข้อมูล (Postgres/MySQL)** — ทำแล้ว (ดู[ต่อฐานข้อมูลตรง ๆ](#ต่อฐานข้อมูลตรง-ๆ-postgresql--mysql)): tech เซฟ connection string กับ query ครั้งเดียว ผู้ใช้เห็นแต่ตาราง · คำสั่งรันในทรานแซกชันอ่านอย่างเดียว ยังเหลือ: ตัวช่วยเลือกตารางแทนการพิมพ์ SQL และเทสต์ที่ต่อฐานข้อมูลจริง (ตอนนี้ทดสอบเฉพาะส่วนที่เป็นฟังก์ชันล้วน) - [ ] **ตัวสำรองตอนไม่มี API key ต้องฉลาดกว่านี้** — ตอนนี้ตอบ `=SUM(A1:A10)` ให้แทบทุกคำถามที่มีคำว่า "รวม" @@ -4015,35 +4029,35 @@ CI: `npm run verify` รวมทุกอย่างไว้แล้ว — (`npm run screenshots`) และ `check:readme` แดงถ้า README ใช้ภาพผิดภาษา · ระหว่างทางเจอและแก้: `SORT(…,-1)` เรียงผิดทาง, toolbar อังกฤษล้นจอ 1366px จนปุ่มเปลี่ยนภาษาโดนตัด, และหน้าต่างข้อมูลสดสองอันไม่ประกาศตัวว่าเป็น dialog - [x] **เลขศูนย์นำหน้าไม่หายแล้ว (#23)** — เบอร์โทร เลขบัตร รหัส เป็นข้อความตั้งแต่ตอนคำนวณ ด้วยกติกาเดียวที่ใช้ร่วมกับ - ตัวเขียน `.xlsx` (ดูหัวข้อ [เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์](#-เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์)) + ตัวเขียน `.xlsx` (ดูหัวข้อ [เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์](#เบอร์โทรและรหัสที่ขึ้นต้นด้วยศูนย์)) - [x] **`SUM`/`AVERAGE`/`COUNT` ข้ามข้อความที่มาจากเซลล์แบบ Excel (#38, #166)** — ทำแล้ว ทั้งช่วงและเซลล์เดี่ยว และชื่อที่ไม่มีใครตั้งเป็น `#NAME?` ทุกที่ - [x] **ข้อมูลสดแบบ CSV ใช้กติกาเดียวกัน (#36)** — ทำแล้ว: เลขศูนย์นำหน้าในข้อมูลสด CSV ไม่หาย -- [x] **เรียงแล้วสูตรในแถวยังถูก + ถามก่อนเรียงถ้าสูตรจะผิด (#48)** — ทำแล้ว (ดูหัวข้อ [เรียงลำดับและกรองข้อมูล](#-เรียงลำดับและกรองข้อมูล)) · ยังเหลือ: สูตรนอกช่วงที่ชี้เข้าช่องเดียวในช่วง -- [x] **เปอร์เซ็นต์ ×100 แบบ Excel และรูปแบบตัวเลขของไฟล์แสดงตามไฟล์ (#53)** — ทำแล้ว (ดูหัวข้อ [จัดรูปแบบเซลล์](#-จัดรูปแบบเซลล์)) · พิมพ์ `50%` เป็นตัวเลข (#52) ทำแล้วเช่นกัน · ยังเหลือ: ตั้งรหัสรูปแบบเองในแอป +- [x] **เรียงแล้วสูตรในแถวยังถูก + ถามก่อนเรียงถ้าสูตรจะผิด (#48)** — ทำแล้ว (ดูหัวข้อ [เรียงลำดับและกรองข้อมูล](#เรียงลำดับและกรองข้อมูล)) · ยังเหลือ: สูตรนอกช่วงที่ชี้เข้าช่องเดียวในช่วง +- [x] **เปอร์เซ็นต์ ×100 แบบ Excel และรูปแบบตัวเลขของไฟล์แสดงตามไฟล์ (#53)** — ทำแล้ว (ดูหัวข้อ [จัดรูปแบบเซลล์](#จัดรูปแบบเซลล์)) · พิมพ์ `50%` เป็นตัวเลข (#52) ทำแล้วเช่นกัน · ยังเหลือ: ตั้งรหัสรูปแบบเองในแอป - [x] **ตัวเลขจากข้างนอกต้องถูก (#52, #55)** — ทำแล้ว: `1,250` `15%` `฿1,500.00` ที่พิมพ์หรือวางเป็นตัวเลขพร้อมรูปแบบ รหัสยังเป็นข้อความ และ merge ในไฟล์ Excel มีค่าที่ซ้ายบนช่องเดียว ยอดรวมตรงกับ Excel -- [x] **วันที่เป็นวันที่จริง (#45)** — ทำแล้ว (ดูหัวข้อ [วันที่และเวลา](#-วันที่และเวลา)): เลข serial แบบ Excel, เวลาไม่หาย +- [x] **วันที่เป็นวันที่จริง (#45)** — ทำแล้ว (ดูหัวข้อ [วันที่และเวลา](#วันที่และเวลา)): เลข serial แบบ Excel, เวลาไม่หาย ตอนนำเข้า, ส่งออกเป็นเซลล์วันที่, รูปแบบของไฟล์แสดงตามไฟล์ และ `###` แทนการตัดท้าย -- [x] **เปิดมาเป็นตารางว่าง + ปุ่มลองกับข้อมูลตัวอย่าง + ไฟล์ใหม่** — ทำแล้ว (ดูหัวข้อ [เริ่มจากตารางว่าง · ไฟล์ใหม่](#-เริ่มจากตารางว่าง--ไฟล์ใหม่)) +- [x] **เปิดมาเป็นตารางว่าง + ปุ่มลองกับข้อมูลตัวอย่าง + ไฟล์ใหม่** — ทำแล้ว (ดูหัวข้อ [เริ่มจากตารางว่าง · ไฟล์ใหม่](#เริ่มจากตารางว่าง--ไฟล์ใหม่)) - [ ] **ผลของสูตรวันที่ได้รูปแบบวันที่เอง** — ตอนนี้เฉพาะ `TODAY`/`NOW`/`DATE` ส่วน `=A1+30` ยังเป็นเลขจนกว่าจะเลือกรูปแบบ - [ ] **กฎจำกัดค่าแบบวันที่** — นำเข้า/ส่งออก date validation ของ Excel (แม่แบบของ ERP ใช้อยู่) ตอนนี้ถูกทิ้งตอนเปิดไฟล์ -- [x] **วันที่แบบไทย (#82)** — ทำแล้ว (ดู [วันที่แบบ พ.ศ.](#-วันที่แบบ-พศ)): ปี พ.ศ. และเดือนไทยอ่านเป็นวันที่ ค.ศ., คำสั่งแปลงเป็นวันที่ +- [x] **วันที่แบบไทย (#82)** — ทำแล้ว (ดู [วันที่แบบ พ.ศ.](#วันที่แบบ-พศ)): ปี พ.ศ. และเดือนไทยอ่านเป็นวันที่ ค.ศ., คำสั่งแปลงเป็นวันที่ สำหรับ `15/01/69` และ `dd/mm/yyyy` ค.ศ., รูปแบบ "วันที่ (พ.ศ.)" ไป-กลับ `.xlsx` - [ ] **ลองกับไฟล์ที่ Excel ภาษาไทยบันทึกเอง** — ทั้งการเปิดไฟล์ที่วันที่ตั้งรูปแบบ พ.ศ. ไว้ และรหัส `[$-107041E]d/m/yyyy;@` ขาออก - ยังไม่ได้เทียบกับไฟล์จริง รอไฟล์ตัวอย่าง (ดู[ข้อจำกัดของวันที่แบบ พ.ศ.](#-วันที่แบบ-พศ)) + ยังไม่ได้เทียบกับไฟล์จริง รอไฟล์ตัวอย่าง (ดู[ข้อจำกัดของวันที่แบบ พ.ศ.](#วันที่แบบ-พศ)) - [ ] **เลขไทยและเวลาแบบ `13.45 น.`** — ยังไม่อ่านเป็นวันที่/เวลา - [ ] **CSV ที่มีช่องขึ้นต้นด้วย `'`** — ตอนนำเข้า `'` ตัวแรกถูกอ่านเป็นเครื่องหมาย "ที่เหลือเป็นข้อความ" แบบ Excel จึงไม่แสดง ถ้าไฟล์ตั้งใจให้ `'` เป็นตัวอักษรจริง (เช่นชื่อ `'s-Hertogenbosch`) ตอนนี้ยังเสียไป -- [x] **รอบแก้จากการทดสอบแบบ blind (usability)** — [เปิดไฟล์ใหม่ไม่ทับงานเดิม](#-เปิดไฟล์ใหม่โดยไม่ทับงานเดิม) พร้อมปุ่มย้อนกลับ, - [แถบคำสั่งบนมือถือ](#-คัดลอก-วาง-เติมลงด้วยนิ้ว), [สีพื้นเซลล์](#-สีพื้นเซลล์), `Ctrl+B/I/U`, ภาษาเริ่มต้นตามเบราว์เซอร์, - [เลย์เอาต์แท็บเล็ต/จอพับ](#-แท็บเล็ตและมือถือจอพับ), พาเนล AI เรียงใหม่ และหมวดสูตรใช้บ่อย · ข้อที่เหลือจากรอบเดียวกันอยู่ด้านล่าง -- [x] **`Tab` แล้ว `Enter` กลับไปคอลัมน์ที่เริ่ม** แบบ Excel — ทำแล้ว ทั้งตอนแก้ในช่องและตอนแค่เลือกช่อง (ดู[คีย์บอร์ดแบบเดียวกับ Excel](#️-คีย์บอร์ดแบบเดียวกับ-excel)) -- [x] **ลากขอบหัวคอลัมน์เพื่อปรับความกว้าง** — ทำแล้ว พร้อมดับเบิลคลิกให้พอดีข้อความ (ดู[ลากปรับความกว้างคอลัมน์](#-ลากปรับความกว้างคอลัมน์)) ยังเหลือ: ปรับด้วยคีย์บอร์ด และลากปรับความสูงแถว -- [x] **เมนูคลิกขวาที่เซลล์บนเดสก์ท็อป** — ทำแล้ว เปิดจากคีย์บอร์ดได้ด้วย `Shift+F10` (ดู[คลิกขวาที่เซลล์](#️-คลิกขวาที่เซลล์)) ยังเหลือ: คอมเมนต์กับจัดรูปแบบในเมนู -- [x] **คีย์บอร์ดมือถือค้างไว้หลังกด Enter** — ทำแล้ว Enter พาเข้าช่องถัดไปพร้อมพิมพ์ต่อ (ดู[ใช้บนมือถือได้](#-ใช้บนมือถือได้)) · ทดสอบใน Chromium แบบจำลองสัมผัส ยังต้องลองบนเครื่องจริงทั้ง iOS และ Android +- [x] **รอบแก้จากการทดสอบแบบ blind (usability)** — [เปิดไฟล์ใหม่ไม่ทับงานเดิม](#เปิดไฟล์ใหม่โดยไม่ทับงานเดิม) พร้อมปุ่มย้อนกลับ, + [แถบคำสั่งบนมือถือ](#คัดลอก-วาง-เติมลงด้วยนิ้ว), [สีพื้นเซลล์](#สีพื้นเซลล์), `Ctrl+B/I/U`, ภาษาเริ่มต้นตามเบราว์เซอร์, + [เลย์เอาต์แท็บเล็ต/จอพับ](#แท็บเล็ตและมือถือจอพับ), พาเนล AI เรียงใหม่ และหมวดสูตรใช้บ่อย · ข้อที่เหลือจากรอบเดียวกันอยู่ด้านล่าง +- [x] **`Tab` แล้ว `Enter` กลับไปคอลัมน์ที่เริ่ม** แบบ Excel — ทำแล้ว ทั้งตอนแก้ในช่องและตอนแค่เลือกช่อง (ดู[คีย์บอร์ดแบบเดียวกับ Excel](#คีย์บอร์ดแบบเดียวกับ-excel)) +- [x] **ลากขอบหัวคอลัมน์เพื่อปรับความกว้าง** — ทำแล้ว พร้อมดับเบิลคลิกให้พอดีข้อความ (ดู[ลากปรับความกว้างคอลัมน์](#ลากปรับความกว้างคอลัมน์)) ยังเหลือ: ปรับด้วยคีย์บอร์ด และลากปรับความสูงแถว +- [x] **เมนูคลิกขวาที่เซลล์บนเดสก์ท็อป** — ทำแล้ว เปิดจากคีย์บอร์ดได้ด้วย `Shift+F10` (ดู[คลิกขวาที่เซลล์](#คลิกขวาที่เซลล์)) ยังเหลือ: คอมเมนต์กับจัดรูปแบบในเมนู +- [x] **คีย์บอร์ดมือถือค้างไว้หลังกด Enter** — ทำแล้ว Enter พาเข้าช่องถัดไปพร้อมพิมพ์ต่อ (ดู[ใช้บนมือถือได้](#ใช้บนมือถือได้)) · ทดสอบใน Chromium แบบจำลองสัมผัส ยังต้องลองบนเครื่องจริงทั้ง iOS และ Android - [x] **หน้าต่างต่อ API ตั้งค่าแหล่งข้อมูล และหน้าต่างเลือกข้อมูล (#122) ปิดด้วย `Escape` ได้แล้ว** — ใช้ hook `useDialogKeys` ตัวเดียวกับหน้าต่างถามตอนนำเข้า: Escape ปิด · Tab วนอยู่ในหน้าต่าง · focus เข้าตอนเปิดและกลับที่ปุ่มเดิมตอนปิด - [x] **สีพื้นในการส่งออก PDF** — ทำแล้ว พร้อมสีตัวอักษร ยังเหลือ: ตัวหนา/ตัวเอียงใน PDF - [ ] **วางจากคลิปบอร์ดของระบบ**ผ่านแถบคำสั่งบนมือถือ -- [x] **หน้า landing สั้นลงสำหรับคนที่ไม่ใช่นักพัฒนา** — ทำแล้ว มือถือ 13.9 → 7.6 จอ (ดู[หน้า landing อธิบายแอป](#-หน้า-landing-อธิบายแอป)) +- [x] **หน้า landing สั้นลงสำหรับคนที่ไม่ใช่นักพัฒนา** — ทำแล้ว มือถือ 13.9 → 7.6 จอ (ดู[หน้า landing อธิบายแอป](#หน้า-landing-อธิบายแอป)) ยังเหลือ: ลองกับคนที่ไม่ใช่นักพัฒนาจริง ๆ ว่าการ์ดที่พับไว้ถูกเปิดไหม หรือภาพพิสูจน์ควรอยู่นอกการพับ - [ ] **Realtime แบบ push (SSE/WebSocket)** แทน polling, และการกรองข้อมูลสดจาก UI ก่อนวาง - [ ] **เชื่อมกับ PaynEat ERP** — ตอบรับแล้ว ยังไม่ได้สร้าง (ดู [docs/payneat-erp.md](docs/payneat-erp.md)): @@ -4053,14 +4067,14 @@ CI: `npm run verify` รวมทุกอย่างไว้แล้ว — **ตั้งใจไม่ทำในสโคปนี้:** - **บริการแก้ร่วมกันที่โฮสต์ให้** — ตัวการแก้ร่วมกัน *ทำแล้ว* แต่ทำบน backend ของผู้ใช้เอง - (ดู [แก้พร้อมกันหลายคน](#-แก้พร้อมกันหลายคน)) การรันเซิร์ฟเวอร์กลางให้ทุกคนใช้คือสิ่งที่ตั้งใจไม่ทำ + (ดู [แก้พร้อมกันหลายคน](#แก้พร้อมกันหลายคน)) การรันเซิร์ฟเวอร์กลางให้ทุกคนใช้คือสิ่งที่ตั้งใจไม่ทำ เพราะจะกลับด้านคุณสมบัติที่ทั้งแอปยืนอยู่บนมัน: ไฟล์ของคุณไม่ออกจากเบราว์เซอร์ไปไหน - **ใช้ไลบรารีคำนวณสูตรสำเร็จรูป** — ตั้งใจเขียนเอนจินเองเพื่อควบคุมพฤติกรรมได้เต็มที่ (ดู - [เอนจินคำนวณสูตร](#-เอนจินคำนวณสูตร)) แม้จะได้ฟังก์ชันน้อยกว่าไลบรารีสำเร็จรูปอย่าง HyperFormula + [เอนจินคำนวณสูตร](#เอนจินคำนวณสูตร)) แม้จะได้ฟังก์ชันน้อยกว่าไลบรารีสำเร็จรูปอย่าง HyperFormula --- -## 👤 ผู้เขียน +## ผู้เขียน สร้างโดย **Suruch Boss** @@ -4068,11 +4082,11 @@ CI: `npm run verify` รวมทุกอย่างไว้แล้ว — - LinkedIn — [linkedin.com/in/suruchboss](https://www.linkedin.com/in/suruchboss) - อีเมล — [bossxiii@gmail.com](mailto:bossxiii@gmail.com) -สนใจโปรเจกต์นี้ อยากคุยเรื่องงาน หรือมีคำถาม ทักมาได้เลยครับ +หากสนใจโปรเจกต์นี้ ต้องการติดต่อเรื่องงาน หรือมีคำถาม ติดต่อได้ทางช่องทางข้างต้น --- -## 📄 License +## License เผยแพร่ภายใต้ [Apache License 2.0](LICENSE) — เอาไปใช้ แก้ไข และใช้ในเชิงพาณิชย์ได้ฟรี diff --git a/docs/payneat-erp.en.md b/docs/payneat-erp.en.md index 188ed364..bc2e1ef8 100644 --- a/docs/payneat-erp.en.md +++ b/docs/payneat-erp.en.md @@ -13,7 +13,7 @@ - **ExcelToGo still stands on its own.** Someone who never uses the ERP sees nothing different. Anything specific to PaynEat lives in a template or in operator configuration, never on the main screen. -- **The ERP is reached through its public API only.** A [database source](../README.en.md#-straight-into-a-database-postgresql--mysql) +- **The ERP is reached through its public API only.** A [database source](../README.en.md#straight-into-a-database-postgresql--mysql) must not be pointed at the ERP's database: its tables are not a contract, and reading them directly steps around the ERP's audit trail. **ExcelToGo cannot enforce this in code** — the list of reachable hosts belongs to the operator — so the ERP should also close the network path to its own database. diff --git a/docs/payneat-erp.md b/docs/payneat-erp.md index 9f9f3ad4..8c47350a 100644 --- a/docs/payneat-erp.md +++ b/docs/payneat-erp.md @@ -13,7 +13,7 @@ - **ExcelToGo ยืนได้เองเหมือนเดิม** คนที่ไม่ได้ใช้ ERP ต้องไม่เห็นอะไรเปลี่ยน ของที่เป็นของ PaynEat โดยเฉพาะอยู่ใน แม่แบบหรือในค่าที่ operator ตั้งเท่านั้น ไม่อยู่บนหน้าหลัก -- **ต่อ ERP ผ่าน public API เท่านั้น** ห้ามใช้ [แหล่งข้อมูลฐานข้อมูล](../README.md#-ต่อฐานข้อมูลตรง-ๆ-postgresql--mysql) +- **ต่อ ERP ผ่าน public API เท่านั้น** ห้ามใช้ [แหล่งข้อมูลฐานข้อมูล](../README.md#ต่อฐานข้อมูลตรง-ๆ-postgresql--mysql) กับฐานข้อมูลของ ERP โครงสร้างตารางของ ERP ไม่ใช่สัญญา และการอ่านตรงข้าม audit trail ของ ERP **ExcelToGo บังคับข้อนี้ด้วยโค้ดไม่ได้** เพราะรายชื่อ host ที่ต่อได้เป็นของ operator ฝั่ง ERP จึงควรปิดทางเครือข่าย ไปยังฐานข้อมูลของตัวเองอีกชั้นหนึ่ง diff --git a/scripts/check-readme.mjs b/scripts/check-readme.mjs index 357c08a6..787fe426 100644 --- a/scripts/check-readme.mjs +++ b/scripts/check-readme.mjs @@ -163,16 +163,16 @@ const featureCount = (text, start, end) => { if (from === -1 || to === -1) return null; return (text.slice(from, to).match(/^### /gm) ?? []).length; }; -const thFeatures = featureCount(docs["README.md"], "## ✨ ฟีเจอร์", "## 🛠 เทคโนโลยีที่ใช้"); -const enFeatures = featureCount(docs["README.en.md"], "## ✨ Features", "## 🛠 Tech stack"); +const thFeatures = featureCount(docs["README.md"], "## ฟีเจอร์", "## เทคโนโลยีที่ใช้"); +const enFeatures = featureCount(docs["README.en.md"], "## Features", "## Tech stack"); if (thFeatures === null || enFeatures === null) fail("couldn't locate the Features section in one of the READMEs"); else if (thFeatures !== enFeatures) fail(`Features sections are out of sync: Thai has ${thFeatures}, English has ${enFeatures}`); else notes.push(`${thFeatures} feature sections, matching in both languages`); // --- 6. …and every feature section should be reachable from the contents ----------------------- for (const [file, text, start, end] of [ - ["README.md", docs["README.md"], "## ✨ ฟีเจอร์", "## 🛠 เทคโนโลยีที่ใช้"], - ["README.en.md", docs["README.en.md"], "## ✨ Features", "## 🛠 Tech stack"], + ["README.md", docs["README.md"], "## ฟีเจอร์", "## เทคโนโลยีที่ใช้"], + ["README.en.md", docs["README.en.md"], "## Features", "## Tech stack"], ]) { const from = text.indexOf(start); const to = text.indexOf(end); diff --git a/src/app/guide/page.tsx b/src/app/guide/page.tsx index ff2ace8d..028c4934 100644 --- a/src/app/guide/page.tsx +++ b/src/app/guide/page.tsx @@ -13,8 +13,8 @@ import { useHydrateLocaleStore } from "@/store/localeStore"; const REPO_URL = "https://github.com/SuruchBoss/ExcelToGo"; /** The README section each language's reader should land on. Anchors follow GitHub's slugs. */ const README_LIVE_DATA = { - th: `${REPO_URL}#-ข้อมูลสดจาก-api--csv-prototype`, - en: `${REPO_URL}/blob/main/README.en.md#-live-data-from-an-api--csv-prototype`, + th: `${REPO_URL}#ข้อมูลสดจาก-api--csv-prototype`, + en: `${REPO_URL}/blob/main/README.en.md#live-data-from-an-api--csv-prototype`, } as const; /** diff --git a/src/features/landing/LandingPage.tsx b/src/features/landing/LandingPage.tsx index ffceb3d3..0b056414 100644 --- a/src/features/landing/LandingPage.tsx +++ b/src/features/landing/LandingPage.tsx @@ -494,7 +494,7 @@ export default function Landing() {

{t.landing.limitsMoreText}

` เป็นตารางที่ถูกต้องตามมาตรฐาน เพียงแต่ไม่ตรงกับลักษณะการใช้งานของแอปนี้ + +ปัญหาทั้งสี่ข้อแก้ไขแล้ว ข้อสรุปจากทุกกรณีเหมือนกัน: ชุดเทสต์ที่ผ่านยืนยันว่าโค้ดทำงานตามที่เทสต์กำหนด +แต่ไม่ได้ยืนยันว่าสิ่งที่กำหนดไว้นั้นถูกต้อง รายละเอียดและวิธีแก้อยู่ในหัวข้อ [ถาม AI หาสูตร](#ถาม-ai-หาสูตร) +และทำซ้ำได้ด้วย `npm run check:ai` + --- -## 🔭 สิ่งที่จะทำต่อ +## สิ่งที่จะทำต่อ -สิ่งที่ยังไม่ได้ทำและเหตุผล — เพื่อให้เห็นว่ารู้ตัวว่าอะไรยังขาด ไม่ใช่ลืม +หัวข้อนี้รวบรวมงานที่ทำแล้ว งานที่ยังไม่ได้ทำ และเหตุผล เพื่อระบุให้ชัดว่าข้อจำกัดใดเป็นข้อจำกัดที่ทราบอยู่แล้ว +ระหว่างที่การพัฒนาพักอยู่ งานที่ค้างทั้งหมดอยู่ใน[รายการ issue ที่เปิดอยู่](https://github.com/SuruchBoss/ExcelToGo/issues) - [x] **CI อัตโนมัติ (GitHub Actions)** — ทำแล้ว: lint → check:readme → test → check:mutants → build ทุก push/PR บน Node 22.12 และ 24 - [x] **บันทึกลงคลาวด์ / sync ข้ามเครื่อง** — ทำแล้วแบบ **"เอา backend ของคุณเองมาต่อ"** - (ดูหัวข้อ ✨ ฟีเจอร์): ต่อ Supabase ของคุณเอง ปิดไว้เป็นค่าเริ่มต้น เพราะโปรเจกต์นี้เป็นโอเพนซอร์ส + (ดูหัวข้อ[ฟีเจอร์](#ฟีเจอร์)): ต่อ Supabase ของคุณเอง ปิดไว้เป็นค่าเริ่มต้น เพราะโปรเจกต์นี้เป็นโอเพนซอร์ส ไม่ใช่บริการที่โฮสต์ให้ การแชร์ให้บัญชีอื่นทำแล้ว พร้อมกับห้องแก้ร่วมกันที่ต้องใช้มัน ยังเหลือ: sync อัตโนมัติ (ประวัติเวอร์ชันทำแล้ว — ดูข้อของมันด้านล่าง) -- [x] **บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้ (#83)** — ทำแล้ว (ดู[บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้](#-บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้)): รูป, รูปร่าง, กราฟและ Pivot Table ของ Excel, มาโคร, ลิงก์ไปไฟล์อื่น และสูตรที่ใช้ฟังก์ชันที่เอนจินไม่มี · ยังเหลือ: ชีตที่ซ่อน/ล็อก, กฎจำกัดค่าและ conditional formatting ชนิดที่ไม่รองรับ, คอมเมนต์แบบ threaded +- [x] **บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้ (#83)** — ทำแล้ว (ดู[บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้](#บอกก่อนว่าไฟล์นี้มีอะไรที่เก็บไว้ไม่ได้)): รูป, รูปร่าง, กราฟและ Pivot Table ของ Excel, มาโคร, ลิงก์ไปไฟล์อื่น และสูตรที่ใช้ฟังก์ชันที่เอนจินไม่มี · ยังเหลือ: ชีตที่ซ่อน/ล็อก, กฎจำกัดค่าและ conditional formatting ชนิดที่ไม่รองรับ, คอมเมนต์แบบ threaded - [ ] **เปิดไฟล์ที่ openpyxl เขียนรูปหรือกราฟไว้** ([#159](https://github.com/SuruchBoss/ExcelToGo/issues/159)) — ตอนนี้เปิดไม่ได้ทั้งไฟล์ -- [x] **ชื่อชีตตามกติกา Excel และไฟล์ส่งออกที่มีค่าจริง (#54, #101)** — ทำแล้ว (ดู[หลายชีตในไฟล์เดียว](#-หลายชีตในไฟล์เดียว)): ชื่อซ้ำหรือผิดกติกาถูกปฏิเสธตอนตั้งและถูกแก้ตอนเปิดงานเก่า, ส่งออกล้มแล้วแอปบอก, ค่าในไฟล์ส่งออกเป็นค่าจริงรวมสูตรข้ามชีต +- [x] **ชื่อชีตตามกติกา Excel และไฟล์ส่งออกที่มีค่าจริง (#54, #101)** — ทำแล้ว (ดู[หลายชีตในไฟล์เดียว](#หลายชีตในไฟล์เดียว)): ชื่อซ้ำหรือผิดกติกาถูกปฏิเสธตอนตั้งและถูกแก้ตอนเปิดงานเก่า, ส่งออกล้มแล้วแอปบอก, ค่าในไฟล์ส่งออกเป็นค่าจริงรวมสูตรข้ามชีต - [ ] **prefix `_xlfn.` ของฟังก์ชันรุ่นใหม่ในไฟล์ส่งออก** ([#156](https://github.com/SuruchBoss/ExcelToGo/issues/156)) — รอเอกสารของ Microsoft หรือไฟล์ที่ Excel จริงสร้าง ก่อนเขียนโค้ด - [ ] **ตัวจับคำสร้าง SUMIF/COUNTIF ให้เอง** (#62–#64 ต่อ) — ตอนนี้คำถามที่มีเงื่อนไขได้ปุ่มเปิดฟอร์ม เพราะการเดาว่าเงื่อนไขอยู่คอลัมน์ไหน คือการเดาอีกแบบ · ถ้าคำถามเอ่ยค่าที่มีอยู่ในคอลัมน์เดียวพอดี (เช่น "เครื่องดื่ม" อยู่ใน B เท่านั้น) ก็เติมฟอร์มให้ล่วงหน้าได้ @@ -3810,22 +3824,22 @@ CI: `npm run verify` รวมทุกอย่างไว้แล้ว — กับยังไม่ยืนยันไว้ด้วย - [ ] **วาดแถวต่อจากแถวสุดท้ายแบบเดียวกับคอลัมน์** — คอลัมน์วาดไปจนสุดจอแล้ว แต่แถวยังหยุดที่แถว 30 ของชีตใหม่ บนจอสูงจึงยังเหลือพื้นที่ว่างใต้ตาราง -- [x] **แก้พร้อมกันหลายคน** — ทำแล้วบน Supabase Realtime ของคุณเอง (ดูหัวข้อ ✨ ฟีเจอร์): +- [x] **แก้พร้อมกันหลายคน** — ทำแล้วบน Supabase Realtime ของคุณเอง (ดูหัวข้อ[ฟีเจอร์](#ฟีเจอร์)): เห็นกันพิมพ์สด ๆ พร้อม presence บอกว่าใครอยู่ช่องไหน ช่องที่กำลังพิมพ์อยู่ไม่ถูกทับกลางคัน และ undo ของเราไม่ลบงานของเขา **ไม่ใช่ CRDT** — พิมพ์ช่องเดียวกันพร้อมกันจะเหลือค่าเดียวและฝั่งที่แพ้ถูกบอก ส่วนแทรก/ลบแถวใช้วิธีบันทึกแล้วให้อีกฝั่งโหลดใหม่ ยังเหลือ: รูปแบบเซลล์ กราฟ และคอมเมนต์ยังไม่ซิงก์สด -- [x] **กราฟ/แผนภูมิ** — ทำแล้ว (ดูหัวข้อ ✨ ฟีเจอร์): แท่ง เส้น วงกลม วาดจากช่วงและอัปเดตตามค่าจริง +- [x] **กราฟ/แผนภูมิ** — ทำแล้ว (ดูหัวข้อ[ฟีเจอร์](#ฟีเจอร์)): แท่ง เส้น วงกลม วาดจากช่วงและอัปเดตตามค่าจริง วางบนตาราง ลากย้ายและย่อขยายได้ ตำแหน่งยึดกับเซลล์ และติดไปกับไฟล์ `.xlsx`/PDF ที่ส่งออก **และกราฟใน `.xlsx` เป็นกราฟจริงที่แก้ต่อได้แล้ว** — เขียน OOXML chart part เองแล้วแทรกลงในไฟล์ ที่ ExcelJS สร้าง ยังเหลือ: กราฟใน PDF ยังเป็นรูปภาพ (jsPDF ไม่มีกราฟของตัวเอง) และกราฟวงกลม ยังวาดได้ทีละชุดข้อมูล -- [x] **สรุปข้อมูล (Pivot)** — ทำแล้ว (ดูหัวข้อ ✨ ฟีเจอร์): จัดกลุ่มหลายคอลัมน์ แตกเป็นคอลัมน์ได้หนึ่งฟิลด์ +- [x] **สรุปข้อมูล (Pivot)** — ทำแล้ว (ดูหัวข้อ[ฟีเจอร์](#ฟีเจอร์)): จัดกลุ่มหลายคอลัมน์ แตกเป็นคอลัมน์ได้หนึ่งฟิลด์ สรุปด้วยผลรวม/จำนวน/ค่าเฉลี่ย/ต่ำสุด/สูงสุด ผลลัพธ์เป็นชีตใหม่ที่ใช้ต่อได้เหมือนข้อมูลปกติ **และชีตสรุปจำที่มาของตัวเองได้แล้ว** — ต้นทางขยับเมื่อไรมีแถบเตือนกับปุ่มสรุปใหม่ ยังเหลือ: ต้องกดเอง ไม่ได้รีเฟรชอัตโนมัติ แตกคอลัมน์ได้ทีละฟิลด์ และยังไม่มีตัวกรองของตัวเอง - [x] **จำกัดจำนวนครั้งที่เรียก `/api/ai/formula`** — ทำแล้ว: 20 ครั้ง/นาที ต่อ IP ตอบ `429` พร้อม `Retry-After` ยังเหลือ: ตัวนับอยู่ในหน่วยความจำของ process จึงกันการยิงมั่วได้ แต่ไม่ใช่เครื่องมือคุมบิลข้ามอินสแตนซ์ -- [x] **รวมเซลล์ (merge cells)** — ทำแล้ว (ดูหัวข้อ ✨ ฟีเจอร์): ปุ่มเดียวรวม/แยก, กลืน merge ที่คร่อมกัน, +- [x] **รวมเซลล์ (merge cells)** — ทำแล้ว (ดูหัวข้อ[ฟีเจอร์](#ฟีเจอร์)): ปุ่มเดียวรวม/แยก, กลืน merge ที่คร่อมกัน, ถามก่อนเฉพาะตอนที่ข้อมูลจะหาย และส่งออกเป็น `` จริงใน `.xlsx` ยังเหลือ: จัดกลางแนวตั้ง และการ freeze เพิ่มเติมนอกเหนือจากหัวแถว/หัวคอลัมน์ที่ sticky อยู่แล้ว - [x] **เพดานขนาดชีต** — ทำแล้ว (`sheetCodec.ts`): **วัดก่อนแก้** ชีต 20,000×26 ที่มีข้อมูลอยู่ช่องเดียว @@ -3834,34 +3848,34 @@ CI: `npm run verify` รวมทุกอย่างไว้แล้ว — ไม่ถึง 5 KB · โมเดลในหน่วยความจำยังเป็นกริดเต็มผืนเหมือนเดิม เพราะ benchmark บอกว่ามันเร็วอยู่แล้ว (20,000×26 คำนวณครั้งแรก 84ms) · เซฟรูปแบบเก่ายังโหลดได้ และตัวกู้ไฟล์ตอน crash อ่านได้ทั้งสองแบบ ยังเหลือ: จำนวนแถวยังเป็นตัวเลขตายตัวต่อชีต ไม่ได้ขยายอัตโนมัติเมื่อพิมพ์เลยขอบ -- [x] **สูตรที่ตอบมาเป็นทั้งตาราง (array formula)** — ทำแล้ว (ดู[สูตรที่ตอบมาเป็นทั้งตาราง (array formula)](#-สูตรที่ตอบมาเป็นทั้งตาราง-array-formula)): +- [x] **สูตรที่ตอบมาเป็นทั้งตาราง (array formula)** — ทำแล้ว (ดู[สูตรที่ตอบมาเป็นทั้งตาราง (array formula)](#สูตรที่ตอบมาเป็นทั้งตาราง-array-formula)): `SEQUENCE` `TRANSPOSE` `UNIQUE` `SORT` `FILTER` ล้นลงเซลล์ข้าง ๆ, `#SPILL!` เมื่อไม่พอดีโดยไม่เขียนอะไรค้างไว้, และตัวดำเนินการที่ทำงานทั้งช่วง (`A1:A9>50` คือเก้าคำตอบ) **ชีตที่มี array คำนวณเฉพาะส่วนที่เปลี่ยนได้แล้ว** — เอา spill เข้า dependency graph แล้ว หนึ่งคีย์สโตรกบนชีต 3,000 แถวที่มี array อยู่ จาก 26.4 ms เหลือ 2.3 ms ยังเหลือ: ยังไม่มี `XMATCH`/`LET`/`LAMBDA` -- [x] **สูตรข้ามชีต** — ทำแล้ว (ดู[สูตรข้ามชีต](#-สูตรข้ามชีต)): `=Sheet2!A1` ชื่อไทยไม่ต้องใส่คำพูด +- [x] **สูตรข้ามชีต** — ทำแล้ว (ดู[สูตรข้ามชีต](#สูตรข้ามชีต)): `=Sheet2!A1` ชื่อไทยไม่ต้องใส่คำพูด staleness ข้ามชีตที่ตามไปทั้งสาย ไม่ใช่แค่ข้อต่อเดียว และวงวนข้ามชีต **บรรทัดนี้เคยเขียนว่า "ส่งออก `.xlsx` แล้วสูตรหาย" ซึ่งไม่จริง** — ตัวเขียนไฟล์ส่ง `{ formula, result }` ลงไปตรง ๆ มานานแล้ว สูตรธรรมดา สูตรข้ามชีต และ `$A$1` ที่ยังเป็น absolute รอดครบทั้งวง ข้อจำกัดที่ไม่มีใครกลับไปตรวจอยู่ได้นานกว่าบั๊กที่มันบรรยาย ตอนนี้เลยมีเทสต์ยึดไว้แทนความจำ — ลองแก้ให้เขียนตัวเลขแทนสูตรดู เทสต์ตกทันทีสี่ข้อ -- [x] **ลากมุมเพื่อเติม** — ทำแล้ว (ดู[ลากมุมเพื่อเติม](#️-ลากมุมเพื่อเติม-fill-handle)): ตัวเลข วันไทย เดือนไทย +- [x] **ลากมุมเพื่อเติม** — ทำแล้ว (ดู[ลากมุมเพื่อเติม](#ลากมุมเพื่อเติม-fill-handle)): ตัวเลข วันไทย เดือนไทย ไตรมาส `Item 08` และสูตรที่การอ้างอิงขยับตาม ยังเหลือ: ลากเข้าด้านในเพื่อล้างค่า และเมนูตัวเลือก ตอนลากด้วยปุ่มขวาแบบ Excel -- [x] **ค้นหาและแทนที่** — ทำแล้ว (ดู[ค้นหาและแทนที่](#-ค้นหาและแทนที่)) ยังเหลือ: ค้นจากค่าที่แสดงด้วย +- [x] **ค้นหาและแทนที่** — ทำแล้ว (ดู[ค้นหาและแทนที่](#ค้นหาและแทนที่)) ยังเหลือ: ค้นจากค่าที่แสดงด้วย ไม่ใช่แค่ข้อความดิบ และ regular expression -- [x] **จัดรูปแบบตามเงื่อนไข (conditional formatting)** — ทำแล้ว (ดูหัวข้อ ✨ ฟีเจอร์): เทียบค่า/ข้อความ/ +- [x] **จัดรูปแบบตามเงื่อนไข (conditional formatting)** — ทำแล้ว (ดูหัวข้อ[ฟีเจอร์](#ฟีเจอร์)): เทียบค่า/ข้อความ/ อันดับ/ไล่สี/แถบยาว เขียนและอ่านกลับจาก `.xlsx` ได้ ยังเหลือ: icon set และกฎแบบสูตรเอง -- [x] **คอมเมนต์ในเซลล์** — ทำแล้ว (ดูหัวข้อ ✨ ฟีเจอร์): โน้ตต่อเซลล์ มีมุมสีส้มบอก เลื่อนตามการแก้ตาราง +- [x] **คอมเมนต์ในเซลล์** — ทำแล้ว (ดูหัวข้อ[ฟีเจอร์](#ฟีเจอร์)): โน้ตต่อเซลล์ มีมุมสีส้มบอก เลื่อนตามการแก้ตาราง และส่งออกเป็นโน้ตของ Excel จริง ยังเหลือ: ชื่อผู้เขียน/เวลา และการตอบกลับเป็นเธรด - [x] **`INDEX`/`MATCH`, `SUMIFS`, `COUNTIFS`, `AVERAGEIFS` และ wildcard ในเงื่อนไข** — ทำแล้ว (ดูหัวข้อ ฟังก์ชันที่รองรับ): ค้นหาย้อนทางซ้ายได้ และนับ/รวม/เฉลี่ยแบบหลายเงื่อนไข - [x] **`XLOOKUP` และ `DATEDIF`** — ทำแล้ว (ดูหัวข้อฟังก์ชันที่รองรับ): ค้นหาแบบที่คอลัมน์คีย์ไม่ต้องอยู่ซ้ายสุด ตรงทั้งหมดเป็นค่าเริ่มต้น กำหนดค่าตอนไม่พบได้ และนับระยะห่างวันที่ครบทั้งหกหน่วย (ระหว่างทางแก้บั๊ก `DAY`/`MONTH`/`YEAR` ที่เพี้ยนไปหนึ่งวันในโซนเวลาฝั่งตะวันตกของ UTC ด้วย) -- [x] **รองรับมือถือ/แท็บเล็ต** — ทำแล้ว (ดูหัวข้อ ✨ ฟีเจอร์): แถบล่างที่มีชื่อทุกปุ่ม, เมนูและแผ่นเครื่องมือเซลล์แทนแถวที่เลื่อนหลุดจอ, +- [x] **รองรับมือถือ/แท็บเล็ต** — ทำแล้ว (ดูหัวข้อ[ฟีเจอร์](#ฟีเจอร์)): แถบล่างที่มีชื่อทุกปุ่ม, เมนูและแผ่นเครื่องมือเซลล์แทนแถวที่เลื่อนหลุดจอ, แตะซ้ำเพื่อแก้เซลล์, ปุ่มขนาด 44px และลากเลือกช่วงด้วยนิ้วผ่านจุดจับที่มุมช่วง (เลื่อนตารางตามเองเมื่อลากถึงขอบ) -- [x] **หาทางต่อ API / ฐานข้อมูลบนมือถือเจอ** — ทำแล้ว: บรรทัดแรกของเมนู, ปุ่ม "ต่อ API ของคุณ" บนสุดของแผงข้อมูลสด และ[คู่มือในเว็บ](#-คู่มือต่อข้อมูลของคุณเอง-ในเว็บ) +- [x] **หาทางต่อ API / ฐานข้อมูลบนมือถือเจอ** — ทำแล้ว: บรรทัดแรกของเมนู, ปุ่ม "ต่อ API ของคุณ" บนสุดของแผงข้อมูลสด และ[คู่มือในเว็บ](#คู่มือต่อข้อมูลของคุณเอง-ในเว็บ) - [x] **ต่อ API ของคุณเองบนเว็บสาธารณะ จากเบราว์เซอร์ (#110)** — ทำแล้ว (ดู[ต่อจากเบราว์เซอร์](#ต่อจากเบราว์เซอร์-restcsv--ใช้บนเว็บนี้ได้เลย)): REST/CSV ที่เครื่องผู้ใช้เปิดถึง ไม่ต้องติดตั้งอะไร URL, header และข้อมูลไม่ผ่านเซิร์ฟเวอร์ · CSP เปิดเฉพาะ origin ที่ผู้ใช้คนนั้นเพิ่ม · ค่า header อยู่แค่ในแท็บ · API ที่ต่อไม่ติดได้รายการตรวจสำหรับ IT แทน error เปล่า ๆ @@ -3873,7 +3887,7 @@ CI: `npm run verify` รวมทุกอย่างไว้แล้ว — - [ ] **ยืนยันตัวตนด้วยคุกกี้/SSO ของบริษัท** — ตอนนี้ `credentials: "omit"` เสมอ การเปิด `include` ต้องให้ API ตอบ `Access-Control-Allow-Credentials` และห้ามใช้ `*` เป็น origin ยังไม่ได้ออกแบบ - [ ] **OAuth** — ยังไม่มี ใช้ได้เฉพาะ header ที่ใส่เอง -- [ ] **ตัวอย่างแบบทำตามทีละขั้น** แทนแหล่งตัวอย่างสามแหล่งที่เอาออกไป — รายการใน `src/lib/server/demoSources.ts` เก็บไว้สำหรับงานนี้ +- [x] **ตัวอย่างให้ลองต่อ** แทนแหล่งตัวอย่างสามแหล่งที่เอาออกไป — ทำแล้ว: [API ตัวอย่างสามตัว](#ลองกับ-api-ตัวอย่าง) ต่อผ่านฟอร์มเดียวกับ API ของผู้ใช้ - [ ] **ทดสอบ Private Network Access กับ IP ภายในจริง** บน Chrome, Edge และ Safari — ข้อสามของรายการตรวจสำหรับ IT ยังไม่เคยลองกับเครื่องจริง จึงเขียนว่า "อาจต้อง" - [ ] **PaynEat ERP เปิด CORS ให้เว็บนี้** — กรณีจริงกรณีแรกของการต่อจากเบราว์เซอร์ ถ้าติดตรงไหนจะได้รู้ว่ารายการตรวจขาดอะไร @@ -3882,7 +3896,7 @@ CI: `npm run verify` รวมทุกอย่างไว้แล้ว — `data/sources.json` ซึ่งเขียนบน Vercel ไม่ได้ ต้องย้ายที่เก็บไปฐานข้อมูล (เช่น Supabase ที่แอปรองรับอยู่แล้ว) ก่อน - [ ] **แยกข้อความ "เซิร์ฟเวอร์ยังไม่เปิดฟีเจอร์" ออกจาก "โทเคนผิด"** — เซิร์ฟเวอร์ตอบรหัสต่างกันอยู่แล้ว (403 / 401) แต่ช่องโทเคนยังแสดงข้อความเดียวรวมกัน ตอนนี้มีลิงก์ไปคู่มือไว้ท้ายแผงแทน -- [ ] **คีย์ลัด Ctrl+B / Ctrl+I / Ctrl+U** — ปุ่มมีแล้ว คีย์ลัดยังไม่มี +- [x] **คีย์ลัด Ctrl+B / Ctrl+I / Ctrl+U** — ทำแล้ว: สลับตัวหนา ตัวเอียง และขีดเส้นใต้ทั้งช่วงที่เลือก (`Cmd` บน Mac) - [ ] **คู่มือในเว็บกับ README ยังไม่มีด่านเทียบกัน** — ถ้า env var หรือขั้นตอนเปลี่ยน ต้องจำแก้ `/guide` เอง - [x] **ตรวจ accessibility อัตโนมัติใน CI** — ทำแล้ว: `npm run check:a11y` รัน axe (WCAG 2.0/2.1/2.2 A+AA) บนทั้งสองหน้าที่ 390px และ 1280px พร้อมเช็กการเลื่อนแนวนอนที่ห้าความกว้าง เป็น job ของตัวเองใน CI @@ -3919,11 +3933,11 @@ CI: `npm run verify` รวมทุกอย่างไว้แล้ว — ตั้ง `NEXT_PUBLIC_ERROR_REPORT_URL` และไม่ส่งอะไรเลยถ้าไม่ตั้ง · สิ่งที่รายงานมีได้เป็นรายการตายตัว key/token/อีเมล/ข้อความไทยถูกล้างออกจาก stack ก่อน และ origin ของ endpoint เข้าไปอยู่ใน `connect-src` ให้เอง ยังเหลือ: error ที่ถูก*จับได้*ยังไม่มีการรายงาน — นำเข้าไฟล์ไม่สำเร็จหรือ fetch ถูกปฏิเสธ ยังเป็นแค่ข้อความบนจอ -- [x] **แสดงว่าสูตรอ่านจากไหน** — ทำแล้ว (ดู[ดูว่าสูตรนี้พูดถึงช่องไหน](#-ดูว่าสูตรนี้พูดถึงช่องไหน)): +- [x] **แสดงว่าสูตรอ่านจากไหน** — ทำแล้ว (ดู[ดูว่าสูตรนี้พูดถึงช่องไหน](#ดูว่าสูตรนี้พูดถึงช่องไหน)): ตีกรอบช่องที่สูตรอ่านบนตาราง และเขียนช่วงเป็นตัวอักษรไว้ข้างแถบสูตร ซึ่งเป็นครึ่งที่ screen reader ใช้ได้ และกลายเป็นครึ่งที่มีประโยชน์กว่า ยังเหลือ: ยังไม่มีทิศทางกลับกัน — ว่าสูตรไหนอ่าน*ช่องนี้*อยู่บ้าง ซึ่งเป็นคำถามที่ถามก่อนจะลบแถว -- [x] **ทำงานตอนไม่มีเน็ต** — ทำแล้ว (ดู[เปิดได้ตอนไม่มีเน็ต](#-เปิดได้ตอนไม่มีเน็ต)): service worker +- [x] **ทำงานตอนไม่มีเน็ต** — ทำแล้ว (ดู[เปิดได้ตอนไม่มีเน็ต](#เปิดได้ตอนไม่มีเน็ต)): service worker ที่เขียนเองแคชตัวแอปไว้ มี manifest ให้ติดตั้งลงหน้าจอหลัก และด่าน e2e ปิดเน็ตแล้วโหลดใหม่เพื่อพิสูจน์ การเปิดหน้าใช้เน็ตก่อนเสมอ เอกสารที่ค้างจึงไม่อยู่นานกว่า build ของมัน · `/api/*` ไม่ถูกแคชเลย เพราะตัวเลขเก่าที่ถูกนำเสนอเหมือนเป็นปัจจุบันคือความล้มเหลวที่รับไม่ได้ที่สุดของโปรเจกต์นี้ @@ -3961,45 +3975,45 @@ CI: `npm run verify` รวมทุกอย่างไว้แล้ว — ส่วนการเลื่อนเคอร์เซอร์เงียบไว้ — โฟกัสอ่านชื่อเซลล์ให้อยู่แล้ว พูดซ้ำแย่กว่าไม่พูด จงใจเงียบพร้อมเทสต์ กำกับ: ข้อมูลสดที่ดึงตามเวลา และตัวเลือกชุดข้อมูลกราฟวงกลมที่เป็น select ซึ่งอ่านตัวเองอยู่แล้ว ยังเหลือ: ยังไม่มี screen reader ตัวจริงมาฟัง -- [x] **หน้ารวมคีย์ลัดในแอป** — ทำแล้ว (ดู[คีย์บอร์ดแบบเดียวกับ Excel](#️-คีย์บอร์ดแบบเดียวกับ-excel)): +- [x] **หน้ารวมคีย์ลัดในแอป** — ทำแล้ว (ดู[คีย์บอร์ดแบบเดียวกับ Excel](#คีย์บอร์ดแบบเดียวกับ-excel)): กด `Ctrl`/`Cmd`+`/` หรือ `F1` หรือปุ่มท้ายแถบชีต รายการคีย์ลัดมีเทสต์อ่านซอร์สของ handler มาเทียบไว้ จะเพิ่ม เปลี่ยนชื่อ หรือลบปุ่มโดยไม่อัปเดตรายการไม่ได้ ยังเหลือ: ครอบคลุมตารางกับ handler ระดับ global ยังไม่รวมปุ่มที่อยู่ในพาเนลแต่ละอัน -- [x] **Import/Export CSV** โดยตรง — ทำแล้ว (ดูหัวข้อ ✨ ฟีเจอร์): เดาตัวคั่นเอง (`,` `;` tab), ตัด/เขียน BOM +- [x] **Import/Export CSV** โดยตรง — ทำแล้ว (ดูหัวข้อ[ฟีเจอร์](#ฟีเจอร์)): เดาตัวคั่นเอง (`,` `;` tab), ตัด/เขียน BOM ให้ Excel อ่านไทยออก, ครอบคำพูดตาม RFC 4180 และส่งออกเป็นค่าที่คำนวณแล้ว **และกัน CSV injection แล้ว** โดยที่ตัวเลขติดลบไม่โดนแตะ และ round trip ของแอปเองยังเหมือนเดิม ยังเหลือ: ไฟล์ที่ไม่ใช่ UTF-8 -- [x] **รองรับ Template จากไฟล์ที่ import เข้ามา** — ทำแล้ว (ดูหัวข้อ ✨ ฟีเจอร์): อ่านการล็อกเซลล์, +- [x] **รองรับ Template จากไฟล์ที่ import เข้ามา** — ทำแล้ว (ดูหัวข้อ[ฟีเจอร์](#ฟีเจอร์)): อ่านการล็อกเซลล์, dropdown และความกว้างคอลัมน์จากไฟล์ที่ protect ไว้ กันการแก้โครงทุกทาง และส่งออกกลับเป็นแม่แบบได้ - [x] **เปิดไฟล์แล้วหน้าตาเหมือนเดิม** — ทำแล้ว: แถบสี, ขนาดตัวอักษร, เส้นขอบ, ความสูงแถว และเซลล์ที่ merge ยังเหลือ: รูปภาพ/กราฟ และการสร้างแม่แบบขึ้นเองในแอป -- [x] **ข้อมูลสดจาก REST API / CSV** — ทำแล้ว (prototype, ดูหัวข้อ ✨ ฟีเจอร์) รีเฟรชแบบ polling +- [x] **ข้อมูลสดจาก REST API / CSV** — ทำแล้ว (prototype, ดูหัวข้อ[ฟีเจอร์](#ฟีเจอร์)) รีเฟรชแบบ polling - [x] **ไล่ดึง API ที่แบ่งหน้า (pagination)** — ทำแล้ว: ตรวจจับเองจาก Link header / ฟิลด์ next / cursor / พารามิเตอร์ใน URL พร้อมบอกผู้ใช้เมื่อดึงมาไม่ครบ - [x] **จัดการ rate limit ให้ผู้ใช้เข้าใจ** — ทำแล้ว: อ่าน `Retry-After`/`X-RateLimit-Reset`, หยุด polling ระหว่างรอจริงๆ, exponential backoff สำหรับ error ทั่วไป, และแสดงเป็นภาษาคนพร้อมนับถอยหลัง - [x] **คำนวณใหม่เฉพาะส่วนที่เปลี่ยน + ตารางใหญ่ไม่หน่วง** — ทำแล้ว (ดูหัวข้อ - [เอนจินคำนวณสูตร](#-เอนจินคำนวณสูตร)): แคช AST + dependency graph ที่อ่านมาจาก AST ทำให้การแก้หนึ่งเซลล์ + [เอนจินคำนวณสูตร](#เอนจินคำนวณสูตร)): แคช AST + dependency graph ที่อ่านมาจาก AST ทำให้การแก้หนึ่งเซลล์ ในชีต 3,000 แถวลดจาก **1,244.9 ms เหลือ 3.6 ms** และตาราง 5,000 แถวมี `