@gui-chat-plugin/spreadsheet 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/engine/calculator.d.ts +15 -3
  2. package/dist/engine/cellBuilder.d.ts +44 -0
  3. package/dist/engine/cellEmpty.d.ts +13 -0
  4. package/dist/engine/cellFormatting.d.ts +25 -0
  5. package/dist/engine/coerce-boolean.d.ts +7 -0
  6. package/dist/engine/condition.d.ts +49 -0
  7. package/dist/engine/date-locale.d.ts +12 -0
  8. package/dist/engine/date-parser.d.ts +7 -1
  9. package/dist/engine/date-utils.d.ts +7 -4
  10. package/dist/engine/datedif.d.ts +13 -0
  11. package/dist/engine/evaluator.d.ts +44 -1
  12. package/dist/engine/financial-math.d.ts +28 -0
  13. package/dist/engine/formatter.d.ts +9 -0
  14. package/dist/engine/formulaError.d.ts +37 -0
  15. package/dist/engine/formulaRefs.d.ts +37 -0
  16. package/dist/engine/functions/lookup-math.d.ts +19 -0
  17. package/dist/engine/functions/statistical-math.d.ts +41 -0
  18. package/dist/engine/functions/text.d.ts +17 -1
  19. package/dist/engine/guards.d.ts +10 -0
  20. package/dist/engine/index.d.ts +11 -0
  21. package/dist/engine/jsonCellLocator.d.ts +27 -0
  22. package/dist/engine/math-ops.d.ts +41 -0
  23. package/dist/engine/numericCoercion.d.ts +38 -0
  24. package/dist/engine/parser.d.ts +0 -24
  25. package/dist/engine/registry.d.ts +30 -2
  26. package/dist/engine/responseDecoder.d.ts +40 -0
  27. package/dist/engine/spreadsheet-errors.d.ts +42 -0
  28. package/dist/engine/textFormat.d.ts +32 -0
  29. package/dist/engine/translateFormula.d.ts +37 -0
  30. package/dist/engine/types.d.ts +17 -3
  31. package/dist/style.css +1 -1
  32. package/dist/vue/cellHighlights.d.ts +42 -0
  33. package/dist/vue/keyboardNav.d.ts +9 -0
  34. package/dist/vue.cjs +71 -71
  35. package/dist/vue.js +6634 -6498
  36. package/package.json +14 -13
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Numeric coercion helpers for the spreadsheet engine.
3
+ *
4
+ * Two intentionally different reads share one string parser (`parseNumericString`):
5
+ * - `registry.toNumber` — lenient, for range aggregation (SUM / AVERAGE / …):
6
+ * anything unreadable becomes 0. PINNED (booleans are 0, not Excel's 1/0).
7
+ * - `toScalarNumber` — strict, for single-value math functions (ABS / SIGN):
8
+ * booleans are 1/0 and non-numeric text is `#VALUE!`, matching Excel.
9
+ *
10
+ * `holdsNumber` asks the question neither read can answer once it has coerced:
11
+ * was there a number here at all, or is this 0 the reading of something else.
12
+ */
13
+ import type { CellValue } from "./types";
14
+ import { type SpreadsheetError } from "./spreadsheet-errors";
15
+ /**
16
+ * Parse a spreadsheet string as a number, or `null` when it holds no number.
17
+ *
18
+ * Mirrors the engine's long-standing lenient read: a percentage, a currency
19
+ * amount, or a thousands-separated value, else a bare `parseFloat` (which reads a
20
+ * leading number out of "12abc"). The branch order is load-bearing — each strips
21
+ * only its own characters, so a string mixing "%" and "$" fails in the first.
22
+ */
23
+ export declare function parseNumericString(value: string): number | null;
24
+ /**
25
+ * Whether a value holds a number at all, under the same reading `toNumber` uses.
26
+ *
27
+ * `toNumber` answers 0 for text, for a boolean and for a genuine 0 alike, so a
28
+ * caller that must tell "no number here" from "the number zero" — COUNT — cannot
29
+ * ask it. Booleans are not numbers here, matching `toNumber`'s PINNED behaviour.
30
+ */
31
+ export declare function holdsNumber(value: CellValue): boolean;
32
+ /**
33
+ * Strict scalar coercion for single-value math functions (ABS, SIGN). Follows
34
+ * Excel's scalar rules: a boolean is 1 / 0 and genuinely non-numeric text is
35
+ * `#VALUE!` rather than a silent 0. A partly-numeric string still yields its
36
+ * leading number, matching the engine's other numeric reads.
37
+ */
38
+ export declare function toScalarNumber(value: CellValue): number | SpreadsheetError;
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Handles Excel A1 notation parsing and conversion
5
5
  */
6
- import type { CellRef, RangeRef } from "./types";
7
6
  /**
8
7
  * Convert Excel column letters to 0-based index
9
8
  * A=0, B=1, ..., Z=25, AA=26, etc.
@@ -20,26 +19,3 @@ export declare function columnToIndex(col: string): number;
20
19
  * @returns Column letters (e.g., "A", "Z", "AA")
21
20
  */
22
21
  export declare function indexToColumn(index: number): string;
23
- /**
24
- * Parse a cell reference to its components
25
- * Supports: A1, $A$1, $A1, A$1, Sheet1!A1, 'My Sheet'!A1
26
- *
27
- * @param ref - Cell reference string
28
- * @returns Parsed cell reference object
29
- */
30
- export declare function parseCellRef(ref: string): CellRef;
31
- /**
32
- * Parse a range reference to its components
33
- * Supports: A1:B10, $A$1:$B$10, Sheet1!A1:B10
34
- *
35
- * @param range - Range reference string
36
- * @returns Parsed range reference object
37
- */
38
- export declare function parseRangeRef(range: string): RangeRef;
39
- /**
40
- * Convert a cell reference object back to A1 notation
41
- *
42
- * @param ref - Cell reference object
43
- * @returns A1 notation string
44
- */
45
- export declare function cellRefToA1(ref: CellRef): string;
@@ -10,11 +10,37 @@ export type CellGetter = (ref: string) => CellValue;
10
10
  export type RangeGetter = (range: string) => CellValue[];
11
11
  export type RawRangeGetter = (range: string) => CellValue[];
12
12
  export interface FunctionContext {
13
+ /** The registry name the formula invoked, so a handler can report a failure
14
+ * the way the evaluator names it. */
15
+ functionName: string;
13
16
  getCellValue: CellGetter;
14
17
  getRangeValues: RangeGetter;
15
- getRangeValuesRaw?: RawRangeGetter;
18
+ getRangeValuesRaw?: RawRangeGetter | undefined;
16
19
  evaluateFormula: (formula: string) => CellValue;
17
20
  }
21
+ /** The too-few-arguments error, raised by the evaluator from the registry's
22
+ * `minArgs` before any handler runs — and by `requiredArg` for the argument a
23
+ * handler actually reads, so both report one wording. */
24
+ export declare const tooFewArgumentsError: (funcName: string, minArgs: number) => Error;
25
+ /** The argument at `index`, which the caller has already established is there —
26
+ * the registry's `minArgs` for a mandatory argument, an `args.length` branch for
27
+ * an optional one. An absent one means that guarantee is wrong, so it raises the
28
+ * arity error the guarantee should have raised. Never a default value: a
29
+ * substituted 0 or "" computes a plausible wrong answer, which is worse than
30
+ * the error the formula deserves. */
31
+ export declare const requiredArg: (context: FunctionContext, args: string[], index: number) => string;
32
+ /** The range reader that keeps every cell, for functions whose answer depends on
33
+ * a range's POSITIONS rather than its numbers.
34
+ *
35
+ * `getRangeValues` drops non-numeric cells, which silently renumbers the rows
36
+ * underneath: a criteria/value pair read that way falls out of alignment and
37
+ * aggregates a different row, and an index returned from it points at the wrong
38
+ * one. SUMIF/AVERAGEIF were moved here in #2358; MATCH/XLOOKUP in #2765.
39
+ *
40
+ * Falls back to the numeric reader because `getRangeValuesRaw` is optional on
41
+ * `FunctionContext` — a host that predates it keeps its old behaviour rather
42
+ * than throwing. */
43
+ export declare const rawRangeReader: (context: FunctionContext) => RangeGetter;
18
44
  export type FunctionHandler = (args: string[], context: FunctionContext) => CellValue;
19
45
  export interface FunctionDefinition {
20
46
  name: string;
@@ -35,7 +61,9 @@ declare class FunctionRegistry {
35
61
  }
36
62
  export declare const functionRegistry: FunctionRegistry;
37
63
  /**
38
- * Helper function to convert a value to a number
64
+ * Lenient numeric coercion for range aggregation: anything unreadable is 0.
65
+ * PINNED behaviour (booleans are 0, not Excel's 1/0) — the string parsing lives
66
+ * in numericCoercion.parseNumericString, shared with the strict scalar read.
39
67
  */
40
68
  export declare function toNumber(value: CellValue): number;
41
69
  /**
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Decode the `/api/files/content` response for a spreadsheet file
3
+ * into an "ok with sheets" / "error with message" discriminated
4
+ * union. Extracted from `fetchSheets` in
5
+ * `src/plugins/spreadsheet/View.vue` where the decision tree was
6
+ * inlined as several nested try/catch + if branches.
7
+ *
8
+ * Pure — no fetch, no refs. Takes the parsed JSON body and returns
9
+ * a result the caller can pattern-match on. Tested in
10
+ * `test/plugins/spreadsheet/engine/test_responseDecoder.ts`.
11
+ */
12
+ import type { SheetData } from "./types.js";
13
+ /** Shape of the `/api/files/content` response we care about. The
14
+ * server returns more fields (kind, size, modifiedMs, …) but this
15
+ * decoder only depends on the three that drive branching. */
16
+ export interface FilesContentResponseLike {
17
+ kind?: string;
18
+ content?: string;
19
+ message?: string;
20
+ }
21
+ export type DecodeResult = {
22
+ kind: "ok";
23
+ sheets: SheetData[];
24
+ } | {
25
+ kind: "error";
26
+ message: string;
27
+ };
28
+ /**
29
+ * Turn a parsed `/files/content` body into an OK/error decision:
30
+ *
31
+ * - `kind` present and not "text" → error with the server's message
32
+ * (e.g. "too-large", "binary"). Spreadsheets only live in text
33
+ * JSON files.
34
+ * - Missing or non-string `content` → error.
35
+ * - `content` is not valid JSON → error.
36
+ * - Parsed `content` is not an array → error (server should never
37
+ * return a non-array but the guard protects downstream render).
38
+ * - Otherwise → ok with the sheets array.
39
+ */
40
+ export declare function decodeSpreadsheetResponse(body: FilesContentResponseLike): DecodeResult;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Formula errors as a distinct VALUE type.
3
+ *
4
+ * A `#NUM!` produced by `SQRT(-1)` and the text `"#NUM!"` produced by
5
+ * `CONCAT("#N","UM!")` used to be the same string, so IFERROR could not tell a
6
+ * real error from text that merely spells one (#2451). An error is now its own
7
+ * value carrying the code, which is what the error-aware functions check; the
8
+ * display pass renders it back to `#NUM!` so cells look unchanged.
9
+ */
10
+ export declare const SPREADSHEET_ERRORS: readonly ["#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A", "#ERROR!"];
11
+ export type SpreadsheetErrorCode = (typeof SPREADSHEET_ERRORS)[number];
12
+ /** A formula error, distinct from any string. `toString` yields the code so the
13
+ * value renders as `#NUM!` wherever the engine coerces a cell value to text. */
14
+ export declare class SpreadsheetError {
15
+ readonly code: SpreadsheetErrorCode;
16
+ constructor(code: SpreadsheetErrorCode);
17
+ toString(): string;
18
+ toJSON(): string;
19
+ }
20
+ export declare const NULL_ERROR: SpreadsheetError;
21
+ export declare const DIV_ZERO_ERROR: SpreadsheetError;
22
+ export declare const VALUE_ERROR: SpreadsheetError;
23
+ export declare const REF_ERROR: SpreadsheetError;
24
+ export declare const NAME_ERROR: SpreadsheetError;
25
+ export declare const NUM_ERROR: SpreadsheetError;
26
+ export declare const NA_ERROR: SpreadsheetError;
27
+ export declare const UNKNOWN_ERROR: SpreadsheetError;
28
+ /** The error value for a code. */
29
+ export declare const spreadsheetError: (code: SpreadsheetErrorCode) => SpreadsheetError;
30
+ /** Whether a value is one of the error CODES as a plain string. Text that spells
31
+ * an error is not an error value — see `isSpreadsheetErrorValue`. */
32
+ export declare function isSpreadsheetError(value: unknown): value is SpreadsheetErrorCode;
33
+ /** Whether a value IS a formula error, as opposed to text that spells one. */
34
+ export declare function isSpreadsheetErrorValue(value: unknown): value is SpreadsheetError;
35
+ /** The code behind a value: an error value's own code, or the code a plain
36
+ * string spells. Used where a literal `#REF!` written into a cell must still
37
+ * poison the arithmetic that reads it, provenance aside. */
38
+ export declare function errorCodeOf(value: unknown): SpreadsheetErrorCode | null;
39
+ /** Whether an evaluated result should be treated as an error: a formula error
40
+ * VALUE, a NaN / infinite number, or a missing value. This is what IFERROR
41
+ * catches — deliberately NOT a look-alike string, which is ordinary text. */
42
+ export declare function isErrorResult(value: unknown): boolean;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Excel number-format patterns for the TEXT function.
3
+ *
4
+ * Deliberately separate from `formatter.formatNumber`, which renders CELL
5
+ * display values: the two disagree on defaults — Excel's `TEXT(0.5,"0%")` is
6
+ * `"50%"`, while a cell carrying the format `0%` has always displayed
7
+ * `"50.00%"` — so sharing the whole path would move every formatted cell in
8
+ * every stored workbook. Only the digit-grouping primitive is shared.
9
+ */
10
+ export interface NumberPattern {
11
+ prefix: string;
12
+ suffix: string;
13
+ useGrouping: boolean;
14
+ integerMinDigits: number;
15
+ minDecimals: number;
16
+ maxDecimals: number;
17
+ isPercent: boolean;
18
+ }
19
+ /**
20
+ * Split a format code into a leading literal, one numeric core and a trailing
21
+ * literal, or `null` when the code uses a feature this module does not render.
22
+ */
23
+ export declare const parseNumberPattern: (pattern: string) => NumberPattern | null;
24
+ /**
25
+ * Render a number the way Excel's TEXT does for the common numeric format
26
+ * codes: `#,##0` grouping, `0.00` fixed decimals, `0.##` optional decimals, a
27
+ * literal prefix/suffix such as `$`, and a `%` that scales by 100.
28
+ *
29
+ * Returns `null` for a format code it does not render, so the caller can keep
30
+ * its own fallback rather than inventing a plausible wrong string.
31
+ */
32
+ export declare const formatWithPattern: (value: number, pattern: string) => string | null;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Excel-formula → JS-expression translation
3
+ *
4
+ * Pure string transforms that turn the Excel-operator form of an
5
+ * already-substituted expression (cell references resolved, functions replaced)
6
+ * into the JavaScript form handed to `new Function`, plus the character
7
+ * allowlists that gate that evaluation. No engine or evaluator state is
8
+ * captured — every function here is input → output only.
9
+ */
10
+ /** Excel `^` exponentiation → JS `**`.
11
+ *
12
+ * Excel's `^` is left-associative (`2^3^2` = `(2^3)^2` = 64); JS `**` is
13
+ * right-associative (`2**3**2` = `2**(3**2)` = 512). This transform does not
14
+ * bridge that difference, so a chained `^` still evaluates the JS way — a known
15
+ * limitation tracked separately (#2359), pinned by the tests. */
16
+ export declare function caretToPow(expr: string): string;
17
+ /** Excel `&` string concatenation → JS `+`, leaving any `&` inside a quoted
18
+ * string literal untouched. A quote toggles the "inside a literal" state only
19
+ * when it is not backslash-escaped. */
20
+ export declare function replaceConcatOperator(expr: string): string;
21
+ /** Excel `=` equality → JS `==`, without disturbing `<=`, `>=` or `!=`. A single
22
+ * `=` is rewritten only when flanked by a non-`<>!` char on the left and a
23
+ * non-`=` on the right.
24
+ *
25
+ * Known limitations, pinned by the tests and tracked separately (#2359): the
26
+ * match consumes both flanking characters so replacements do not overlap
27
+ * (`5=6=7` → `5==6=7`, only the first rewritten); a `=` at either end of the
28
+ * expression has no left/right neighbour and is left alone (`5=` → `5=`); and a
29
+ * hand-typed `==` becomes `===` (its second `=` matches). Excel never emits the
30
+ * last two, so they only bite malformed input. */
31
+ export declare function rewriteComparisonEq(expr: string): string;
32
+ /** Whether an expression contains only the characters an arithmetic evaluation
33
+ * may see: digits, ` + - * / ( ) . ` and spaces. Gates `new Function`. */
34
+ export declare function isSafeArithmetic(expr: string): boolean;
35
+ /** Whether an expression contains only the characters a comparison evaluation
36
+ * may see: the arithmetic set plus ` < > ! = `. Gates `new Function`. */
37
+ export declare function isSafeComparison(expr: string): boolean;
@@ -1,9 +1,14 @@
1
1
  /**
2
2
  * Spreadsheet Engine Type Definitions
3
3
  */
4
- export type CellValue = number | string | boolean;
4
+ import type { SpreadsheetError } from "./spreadsheet-errors";
5
+ /** A value as it is STORED in a cell and serialized to the workbook JSON. A
6
+ * formula error is never stored — it only exists as a computed result. */
7
+ export type StoredCellValue = number | string | boolean;
8
+ /** A value as the engine COMPUTES it: a stored value, or a formula error. */
9
+ export type CellValue = StoredCellValue | SpreadsheetError;
5
10
  export interface SpreadsheetCell {
6
- v: CellValue;
11
+ v: StoredCellValue;
7
12
  f?: string;
8
13
  }
9
14
  export interface SheetData {
@@ -46,7 +51,16 @@ export interface CalculationError {
46
51
  error: string;
47
52
  type: "circular" | "invalid_ref" | "div_zero" | "syntax" | "unknown";
48
53
  }
49
- export interface EngineOptions {
54
+ /** Per-calculation settings that change how cell CONTENT is read, as opposed
55
+ * to how the engine runs. Passed down explicitly so the engine stays pure —
56
+ * no ambient locale, no import-time state. */
57
+ export interface CalculateOptions {
58
+ /** Read an ambiguous `A/B/YYYY` date as day-first. Only affects dates whose
59
+ * two leading numbers are both 12 or under; anything else decides itself.
60
+ * See engine/date-locale.ts for how a locale maps onto this. */
61
+ preferDDMMYYYY?: boolean;
62
+ }
63
+ export interface EngineOptions extends CalculateOptions {
50
64
  maxIterations?: number;
51
65
  enableCrossSheetRefs?: boolean;
52
66
  strictMode?: boolean;
package/dist/style.css CHANGED
@@ -1,3 +1,3 @@
1
1
  /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
- @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-900:oklch(39.3% .095 152.535);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-800:oklch(27.8% .033 256.848);--color-white:#fff;--spacing:.25rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:var(--spacing)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.contents{display:contents}.flex{display:flex}.table{display:table}.h-\[600px\]{height:600px}.max-h-64{max-height:calc(var(--spacing) * 64)}.min-h-full{min-height:100%}.max-w-4xl{max-width:var(--container-4xl)}.max-w-\[200px\]{max-width:200px}.flex-shrink{flex-shrink:1}.border-collapse{border-collapse:collapse}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-gray-200{border-color:var(--color-gray-200)}.border-green-200{border-color:var(--color-green-200)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-white{background-color:var(--color-white)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-8{padding:calc(var(--spacing) * 8)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-800{color:var(--color-gray-800)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:border-green-300:hover{border-color:var(--color-green-300)}.hover\:bg-green-200:hover{background-color:var(--color-green-200)}}@media (prefers-color-scheme:dark){.dark\:bg-green-900{background-color:var(--color-green-900)}.dark\:text-gray-200{color:var(--color-gray-200)}.dark\:text-gray-400{color:var(--color-gray-400)}.dark\:text-green-300{color:var(--color-green-300)}}}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}.spreadsheet-container[data-v-46c4c1c7]{background:#fff;flex-direction:column;width:100%;height:100%;display:flex}.spreadsheet-content-wrapper[data-v-46c4c1c7]{flex:1;min-height:0;overflow-y:auto}.header[data-v-46c4c1c7]{justify-content:space-between;align-items:center;margin-bottom:1em;display:flex}.title[data-v-46c4c1c7]{margin:0;font-size:2em;font-weight:700}.button-group[data-v-46c4c1c7]{gap:.5em;display:flex}.download-btn[data-v-46c4c1c7]{color:#fff;cursor:pointer;border:none;border-radius:4px;align-items:center;gap:.5em;padding:.5em 1em;font-size:.9em;transition:background-color .2s;display:flex}.excel-btn[data-v-46c4c1c7]{background-color:#217346}.excel-btn[data-v-46c4c1c7]:hover{background-color:#1e6a3f}.excel-btn[data-v-46c4c1c7]:active{background-color:#1a5c36}.download-btn .material-icons[data-v-46c4c1c7]{font-size:1.2em}.sheet-tabs[data-v-46c4c1c7]{border-bottom:2px solid #e0e0e0;gap:.25em;margin-bottom:1em;display:flex}.sheet-tab[data-v-46c4c1c7]{cursor:pointer;color:#666;background:#f5f5f5;border:none;border-top-left-radius:4px;border-top-right-radius:4px;padding:.5em 1em;font-size:.9em;transition:background-color .2s}.sheet-tab[data-v-46c4c1c7]:hover{background:#e8e8e8}.sheet-tab.active[data-v-46c4c1c7]{color:#333;background:#fff;border-bottom:2px solid #fff;margin-bottom:-2px;font-weight:500}.table-container[data-v-46c4c1c7]{background:#fff;overflow-x:auto}.table-container[data-v-46c4c1c7] table{border-collapse:collapse;width:100%;font-family:Segoe UI,Arial,sans-serif;font-size:.9em}.table-container[data-v-46c4c1c7] td,.table-container[data-v-46c4c1c7] th{text-align:left;border:1px solid #d0d0d0;padding:.5em .75em}.table-container[data-v-46c4c1c7] th{color:#333;background-color:#f5f5f5;font-weight:600}.table-container[data-v-46c4c1c7] tr:nth-child(2n){background-color:#fafafa}.table-container[data-v-46c4c1c7] tr:hover{background-color:#f0f0f0}.table-container[data-v-46c4c1c7] .cell-editing{outline-offset:-2px;background-color:#e8f5e9!important;outline:2px solid #217346!important}.table-container[data-v-46c4c1c7] .cell-referenced{outline-offset:-2px;background-color:#fff3e0!important;outline:2px solid #ff9800!important}.error[data-v-46c4c1c7]{color:#c62828;background:#ffebee;border-radius:4px;margin:1em 0;padding:1em}.spreadsheet-source[data-v-46c4c1c7]{background:#f5f5f5;border-top:1px solid #e0e0e0;flex-shrink:0;padding:.5rem;font-family:monospace;font-size:.85rem}.spreadsheet-source summary[data-v-46c4c1c7]{cursor:pointer;-webkit-user-select:none;user-select:none;color:#333;background:#e8e8e8;border-radius:4px;padding:.5rem;font-weight:500}.spreadsheet-source[open] summary[data-v-46c4c1c7]{margin-bottom:.5rem}.spreadsheet-source summary[data-v-46c4c1c7]:hover{background:#d8d8d8}.spreadsheet-editor[data-v-46c4c1c7]{color:#333;resize:vertical;background:#fff;border:1px solid #ccc;border-radius:4px;width:100%;height:40vh;margin-bottom:.5rem;padding:1rem;font-family:Courier New,monospace;font-size:.9rem;line-height:1.5}.spreadsheet-editor[data-v-46c4c1c7]:focus{border-color:#217346;outline:none;box-shadow:0 0 0 2px #2173461a}.apply-btn[data-v-46c4c1c7]{color:#fff;cursor:pointer;background:#217346;border:none;border-radius:4px;padding:.5rem 1rem;font-size:.9rem;font-weight:500;transition:background .2s}.apply-btn[data-v-46c4c1c7]:hover{background:#1e6a3f}.apply-btn[data-v-46c4c1c7]:active{background:#1a5c36}.apply-btn[data-v-46c4c1c7]:disabled{color:#666;cursor:not-allowed;opacity:.6;background:#ccc}.apply-btn[data-v-46c4c1c7]:disabled:hover{background:#ccc}.mini-editor-panel[data-v-46c4c1c7]{background:#f8f8f8;border-top:1px solid #d0d0d0;flex-shrink:0}.mini-editor-content[data-v-46c4c1c7]{align-items:center;gap:.5rem;padding:.5rem 1rem;display:flex}.cell-ref[data-v-46c4c1c7]{color:#217346;min-width:2rem;font-family:monospace;font-size:.85rem;font-weight:600}.radio-group[data-v-46c4c1c7]{border-right:1px solid #d0d0d0;gap:.75rem;padding-right:.75rem;display:flex}.radio-option[data-v-46c4c1c7]{cursor:pointer;color:#555;align-items:center;gap:.25rem;font-size:.85rem;display:flex}.radio-option input[type=radio][data-v-46c4c1c7]{cursor:pointer;width:.9rem;height:.9rem}.form-input[data-v-46c4c1c7]{border:1px solid #ccc;border-radius:3px;flex:1;min-width:120px;padding:.4rem .6rem;font-family:inherit;font-size:.85rem;transition:border-color .2s}.form-input[data-v-46c4c1c7]:focus{border-color:#217346;outline:none;box-shadow:0 0 0 2px #2173461a}.form-input[data-v-46c4c1c7]::placeholder{color:#999;font-size:.8rem}.save-btn[data-v-46c4c1c7],.cancel-btn[data-v-46c4c1c7]{cursor:pointer;border:none;border-radius:3px;padding:.4rem .8rem;font-size:.85rem;font-weight:500;transition:background .2s}.save-btn[data-v-46c4c1c7]{color:#fff;background:#217346}.save-btn[data-v-46c4c1c7]:hover{background:#1e6a3f}.cancel-btn[data-v-46c4c1c7]{color:#666;background:0 0;padding:.4rem;font-size:1.2rem;line-height:1}.cancel-btn[data-v-46c4c1c7]:hover{color:#333;background:#e0e0e0}
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-green-50:oklch(98.2% .018 155.826);--color-green-100:oklch(96.2% .044 156.743);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-900:oklch(39.3% .095 152.535);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-800:oklch(27.8% .033 256.848);--color-white:#fff;--spacing:.25rem;--container-4xl:56rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--font-weight-medium:500;--radius-md:.375rem;--radius-lg:.5rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.row-2{grid-row:2}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mx-auto{margin-inline:auto}.mt-1{margin-top:var(--spacing)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.inline{display:inline}.table{display:table}.h-\[600px\]{height:600px}.max-h-64{max-height:calc(var(--spacing) * 64)}.min-h-full{min-height:100%}.max-w-4xl{max-width:var(--container-4xl)}.max-w-\[200px\]{max-width:200px}.flex-shrink{flex-shrink:1}.border-collapse{border-collapse:collapse}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.resize{resize:both}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-2{gap:calc(var(--spacing) * 2)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-gray-200{border-color:var(--color-gray-200)}.border-green-200{border-color:var(--color-green-200)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-green-50{background-color:var(--color-green-50)}.bg-green-100{background-color:var(--color-green-100)}.bg-white{background-color:var(--color-white)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-8{padding:calc(var(--spacing) * 8)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-800{color:var(--color-gray-800)}.text-green-600{color:var(--color-green-600)}.text-green-700{color:var(--color-green-700)}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}@media (hover:hover){.hover\:border-green-300:hover{border-color:var(--color-green-300)}.hover\:bg-green-200:hover{background-color:var(--color-green-200)}}@media (prefers-color-scheme:dark){.dark\:bg-green-900{background-color:var(--color-green-900)}.dark\:text-gray-200{color:var(--color-gray-200)}.dark\:text-gray-400{color:var(--color-gray-400)}.dark\:text-green-300{color:var(--color-green-300)}}}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}.spreadsheet-container[data-v-1d9beb6d]{background:#fff;flex-direction:column;width:100%;height:100%;display:flex}.spreadsheet-content-wrapper[data-v-1d9beb6d]{flex:1;min-height:0;overflow-y:auto}.header[data-v-1d9beb6d]{justify-content:space-between;align-items:center;margin-bottom:1em;display:flex}.title[data-v-1d9beb6d]{margin:0;font-size:2em;font-weight:700}.button-group[data-v-1d9beb6d]{gap:.5em;display:flex}.download-btn[data-v-1d9beb6d]{color:#fff;cursor:pointer;border:none;border-radius:4px;align-items:center;gap:.5em;padding:.5em 1em;font-size:.9em;transition:background-color .2s;display:flex}.excel-btn[data-v-1d9beb6d]{background-color:#217346}.excel-btn[data-v-1d9beb6d]:hover{background-color:#1e6a3f}.excel-btn[data-v-1d9beb6d]:active{background-color:#1a5c36}.download-btn .material-icons[data-v-1d9beb6d]{font-size:1.2em}.sheet-tabs[data-v-1d9beb6d]{border-bottom:2px solid #e0e0e0;gap:.25em;margin-bottom:1em;display:flex}.sheet-tab[data-v-1d9beb6d]{cursor:pointer;color:#666;background:#f5f5f5;border:none;border-top-left-radius:4px;border-top-right-radius:4px;padding:.5em 1em;font-size:.9em;transition:background-color .2s}.sheet-tab[data-v-1d9beb6d]:hover{background:#e8e8e8}.sheet-tab.active[data-v-1d9beb6d]{color:#333;background:#fff;border-bottom:2px solid #fff;margin-bottom:-2px;font-weight:500}.table-container[data-v-1d9beb6d]{background:#fff;overflow-x:auto}.table-container[data-v-1d9beb6d] table{border-collapse:collapse;width:100%;font-family:Segoe UI,Arial,sans-serif;font-size:.9em}.table-container[data-v-1d9beb6d] td,.table-container[data-v-1d9beb6d] th{text-align:left;border:1px solid #d0d0d0;padding:.5em .75em}.table-container[data-v-1d9beb6d] th{color:#333;background-color:#f5f5f5;font-weight:600}.table-container[data-v-1d9beb6d] tr:nth-child(2n){background-color:#fafafa}.table-container[data-v-1d9beb6d] tr:hover{background-color:#f0f0f0}.table-container[data-v-1d9beb6d] .cell-editing{outline-offset:-2px;background-color:#e8f5e9!important;outline:2px solid #217346!important}.table-container[data-v-1d9beb6d] .cell-referenced{outline-offset:-2px;background-color:#fff3e0!important;outline:2px solid #ff9800!important}.error[data-v-1d9beb6d]{color:#c62828;background:#ffebee;border-radius:4px;margin:1em 0;padding:1em}.spreadsheet-source[data-v-1d9beb6d]{background:#f5f5f5;border-top:1px solid #e0e0e0;flex-shrink:0;padding:.5rem;font-family:monospace;font-size:.85rem}.spreadsheet-source summary[data-v-1d9beb6d]{cursor:pointer;-webkit-user-select:none;user-select:none;color:#333;background:#e8e8e8;border-radius:4px;padding:.5rem;font-weight:500}.spreadsheet-source[open] summary[data-v-1d9beb6d]{margin-bottom:.5rem}.spreadsheet-source summary[data-v-1d9beb6d]:hover{background:#d8d8d8}.spreadsheet-editor[data-v-1d9beb6d]{color:#333;resize:vertical;background:#fff;border:1px solid #ccc;border-radius:4px;width:100%;height:40vh;margin-bottom:.5rem;padding:1rem;font-family:Courier New,monospace;font-size:.9rem;line-height:1.5}.spreadsheet-editor[data-v-1d9beb6d]:focus{border-color:#217346;outline:none;box-shadow:0 0 0 2px #2173461a}.apply-btn[data-v-1d9beb6d]{color:#fff;cursor:pointer;background:#217346;border:none;border-radius:4px;padding:.5rem 1rem;font-size:.9rem;font-weight:500;transition:background .2s}.apply-btn[data-v-1d9beb6d]:hover{background:#1e6a3f}.apply-btn[data-v-1d9beb6d]:active{background:#1a5c36}.apply-btn[data-v-1d9beb6d]:disabled{color:#666;cursor:not-allowed;opacity:.6;background:#ccc}.apply-btn[data-v-1d9beb6d]:disabled:hover{background:#ccc}.mini-editor-panel[data-v-1d9beb6d]{background:#f8f8f8;border-top:1px solid #d0d0d0;flex-shrink:0}.mini-editor-content[data-v-1d9beb6d]{align-items:center;gap:.5rem;padding:.5rem 1rem;display:flex}.cell-ref[data-v-1d9beb6d]{color:#217346;min-width:2rem;font-family:monospace;font-size:.85rem;font-weight:600}.radio-group[data-v-1d9beb6d]{border-right:1px solid #d0d0d0;gap:.75rem;padding-right:.75rem;display:flex}.radio-option[data-v-1d9beb6d]{cursor:pointer;color:#555;align-items:center;gap:.25rem;font-size:.85rem;display:flex}.radio-option input[type=radio][data-v-1d9beb6d]{cursor:pointer;width:.9rem;height:.9rem}.form-input[data-v-1d9beb6d]{border:1px solid #ccc;border-radius:3px;flex:1;min-width:120px;padding:.4rem .6rem;font-family:inherit;font-size:.85rem;transition:border-color .2s}.form-input[data-v-1d9beb6d]:focus{border-color:#217346;outline:none;box-shadow:0 0 0 2px #2173461a}.form-input[data-v-1d9beb6d]::placeholder{color:#999;font-size:.8rem}.save-btn[data-v-1d9beb6d],.cancel-btn[data-v-1d9beb6d]{cursor:pointer;border:none;border-radius:3px;padding:.4rem .8rem;font-size:.85rem;font-weight:500;transition:background .2s}.save-btn[data-v-1d9beb6d]{color:#fff;background:#217346}.save-btn[data-v-1d9beb6d]:hover{background:#1e6a3f}.cancel-btn[data-v-1d9beb6d]{color:#666;background:0 0;padding:.4rem;font-size:1.2rem;line-height:1}.cancel-btn[data-v-1d9beb6d]:hover{color:#333;background:#e0e0e0}
3
3
  /*$vite$:1*/
@@ -0,0 +1,42 @@
1
+ /**
2
+ * DOM helpers for the mini-editor cell-highlight pass. Extracted from
3
+ * the post-flush watch in `src/vue/View.vue`, which
4
+ * had a cognitive complexity of 30 driven by four levels of nested
5
+ * optional chaining + loops.
6
+ *
7
+ * These helpers are side-effectful by nature (they add/remove CSS
8
+ * classes on DOM nodes) but each one is small enough that its
9
+ * behaviour is obvious. Unit-testable with a minimal mock DOM; see
10
+ * `tests/vue/test_cellHighlights.ts`.
11
+ */
12
+ /** Minimal DOM surface the helpers need. Defined here so tests can
13
+ * pass plain objects without pulling in jsdom. */
14
+ export interface HighlightableElement {
15
+ classList: {
16
+ add: (cls: string) => void;
17
+ remove: (cls: string) => void;
18
+ };
19
+ }
20
+ export interface HighlightableRow {
21
+ querySelectorAll: (selector: string) => ArrayLike<HighlightableElement>;
22
+ }
23
+ export interface HighlightableTable {
24
+ querySelectorAll: (selector: string) => ArrayLike<HighlightableRow>;
25
+ }
26
+ export interface HighlightableContainer {
27
+ querySelector: ((selector: "#spreadsheet-table") => HighlightableTable | null) & ((selector: string) => HighlightableElement | null);
28
+ querySelectorAll: (selector: string) => ArrayLike<HighlightableElement> & Iterable<HighlightableElement>;
29
+ }
30
+ export interface CellCoord {
31
+ row: number;
32
+ col: number;
33
+ }
34
+ /** Remove both kinds of highlight classes from the container. */
35
+ export declare function clearCellHighlights(container: HighlightableContainer | null | undefined): void;
36
+ /** Add `className` to the <td> at (row, col) of the given table.
37
+ * No-op if the row or cell doesn't exist. */
38
+ export declare function highlightCell(table: HighlightableTable | null | undefined, coord: CellCoord, className: string): void;
39
+ /** Apply the editing cell + referenced cells highlights. Looks up
40
+ * the #spreadsheet-table inside the container and no-ops if the
41
+ * table hasn't rendered yet. */
42
+ export declare function applyCellHighlights(container: HighlightableContainer | null | undefined, editingCell: CellCoord | null, references: readonly CellCoord[]): void;
@@ -0,0 +1,9 @@
1
+ export interface CellPosition {
2
+ row: number;
3
+ col: number;
4
+ }
5
+ export interface SheetLike {
6
+ data?: unknown[][];
7
+ }
8
+ export declare function getArrowKeyOffset(key: string, row: number, col: number): CellPosition | null;
9
+ export declare function isWithinSheetBounds(sheet: SheetLike | null | undefined, row: number, col: number): boolean;