Compare commits

..

1 Commits

Author SHA1 Message Date
Nicolás Hatcher
ae53901a24 UPDATE: Updates docs on Errors in IronCalc 2024-12-15 07:52:52 +01:00
29 changed files with 586 additions and 573 deletions

View File

@@ -99,7 +99,7 @@ fn cut_paste() {
// paste in cell D4 (4, 4)
model
.paste_from_clipboard(0, (1, 1, 2, 2), &copy.data, true)
.paste_from_clipboard((1, 1, 2, 2), &copy.data, true)
.unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));
@@ -119,26 +119,6 @@ fn cut_paste() {
assert_eq!(model.get_cell_content(0, 2, 2), Ok("".to_string()));
}
#[test]
fn cut_paste_different_sheet() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
model.set_user_input(0, 1, 1, "42").unwrap();
model.set_selected_range(1, 1, 1, 1).unwrap();
let copy = model.copy_to_clipboard().unwrap();
model.new_sheet().unwrap();
model.set_selected_sheet(1).unwrap();
model.set_selected_cell(4, 4).unwrap();
// paste in cell D4 (4, 4) of Sheet2
model
.paste_from_clipboard(0, (1, 1, 1, 1), &copy.data, true)
.unwrap();
assert_eq!(model.get_cell_content(1, 4, 4), Ok("42".to_string()));
assert_eq!(model.get_cell_content(0, 1, 1), Ok("".to_string()));
}
#[test]
fn copy_paste_internal() {
let mut model = UserModel::new_empty("model", "en", "UTC").unwrap();
@@ -172,7 +152,7 @@ fn copy_paste_internal() {
// paste in cell D4 (4, 4)
model
.paste_from_clipboard(0, (1, 1, 2, 2), &copy.data, false)
.paste_from_clipboard((1, 1, 2, 2), &copy.data, false)
.unwrap();
assert_eq!(model.get_cell_content(0, 4, 4), Ok("42".to_string()));

View File

@@ -39,7 +39,6 @@ pub struct ClipboardCell {
pub struct Clipboard {
pub(crate) csv: String,
pub(crate) data: ClipboardData,
pub(crate) sheet: u32,
pub(crate) range: (i32, i32, i32, i32),
}
@@ -1521,7 +1520,6 @@ impl UserModel {
Ok(Clipboard {
csv,
data,
sheet,
range: (row_start, column_start, row_end, column_end),
})
}
@@ -1529,7 +1527,6 @@ impl UserModel {
/// Paste text that we copied
pub fn paste_from_clipboard(
&mut self,
source_sheet: u32,
source_range: ClipboardTuple,
clipboard: &ClipboardData,
is_cut: bool,
@@ -1620,17 +1617,17 @@ impl UserModel {
let old_value = self
.model
.workbook
.worksheet(source_sheet)?
.worksheet(sheet)?
.cell(row, column)
.cloned();
diff_list.push(Diff::CellClearContents {
sheet: source_sheet,
sheet,
row,
column,
old_value: Box::new(old_value),
});
self.model.cell_clear_contents(source_sheet, row, column)?;
self.model.cell_clear_contents(sheet, row, column)?;
}
}
}

View File

@@ -169,22 +169,20 @@ clipboard_types = r"""
paste_from_clipboard = r"""
/**
* @param {number} source_sheet
* @param {any} source_range
* @param {any} clipboard
* @param {boolean} is_cut
*/
pasteFromClipboard(source_sheet: number, source_range: any, clipboard: any, is_cut: boolean): void;
pasteFromClipboard(source_range: any, clipboard: any, is_cut: boolean): void;
"""
paste_from_clipboard_types = r"""
/**
* @param {number} source_sheet
* @param {[number, number, number, number]} source_range
* @param {ClipboardData} clipboard
* @param {boolean} is_cut
*/
pasteFromClipboard(source_sheet: number, source_range: [number, number, number, number], clipboard: ClipboardData, is_cut: boolean): void;
pasteFromClipboard(source_range: [number, number, number, number], clipboard: ClipboardData, is_cut: boolean): void;
"""
def fix_types(text):

View File

@@ -520,7 +520,6 @@ impl Model {
#[wasm_bindgen(js_name = "pasteFromClipboard")]
pub fn paste_from_clipboard(
&mut self,
source_sheet: u32,
source_range: JsValue,
clipboard: JsValue,
is_cut: bool,
@@ -530,7 +529,7 @@ impl Model {
let clipboard: ClipboardData =
serde_wasm_bindgen::from_value(clipboard).map_err(|e| to_js_error(e.to_string()))?;
self.model
.paste_from_clipboard(source_sheet, source_range, &clipboard, is_cut)
.paste_from_clipboard(source_range, &clipboard, is_cut)
.map_err(|e| to_js_error(e.to_string()))
}

View File

@@ -58,6 +58,30 @@ export default defineConfig({
text: "Error Types",
link: "/features/error-types",
},
{
text: "Value Types",
link: "/features/value-types",
},
{
text: "Optional arguments",
link: "/features/optional-arguments",
},
{
text: "Units",
link: "/features/units",
},
{
text: "Implicit Intersection",
link: "/features/implicit-intersection",
},
{
text: "Arrays and array formulas",
link: "/features/arrays",
},
{
text: "Dynamic Arrays",
link: "/features/dynamic-arrays",
},
{
text: "Unsupported Features",
link: "/features/unsupported-features",

View File

@@ -0,0 +1,15 @@
---
layout: doc
outline: deep
lang: en-US
---
# Arrays
::: warning
**Note:** This page is in construction 🚧
:::
::: warning
**Note:** This feature is not yet available on IronCalc 🚧
:::

View File

@@ -0,0 +1,15 @@
---
layout: doc
outline: deep
lang: en-US
---
# Dynamic Arrays
::: warning
**Note:** This page is in construction 🚧
:::
::: warning
**Note:** This feature is not yet available on IronCalc 🚧
:::

View File

@@ -10,28 +10,36 @@ lang: en-US
**Note:** This page is in construction 🚧
:::
When working with formulas, you may encounter these common errors:
The result of a formula is sometimes an _error_. In some situations those errors are expected and your formulas might be dealing with them.
The error `#N/A` might signal that there is no data to evaluate the formula yet. Maybe the payrol has not been introduced for that month just yet.
---
Some other errors like `#SPILL!`, `#CIRC!` or `#ERROR!` signal an error in your spreadsheet logic and must be corrected.
### **`#ERROR!`**
The first kind of errors or 'common errors' are found in other spreadsheet engines like Excel while other errrors like `#ERROR!` or `#N/IMPL` are particular to IronCalc.
**Cause:** General formula issue, like syntax errors or invalid references.
**Fix:** Check the formula for mistakes or invalid cell references.
---
## Common Errors
### **`#VALUE!`**
**Cause:** Mismatched data types (e.g., text used where numbers are expected).
**Fix:** Ensure input types are correct; convert text to numbers if needed.
It might be caused by mismatched data types (e.g., text used where numbers are expected):
---
```
5+"two"
```
The engine doesn't know how to add the number `5` to the string `two` resulting in a `#VALUE!`.
It is an actual error in your spreadsheet. It indicates that the formula isnt working as intended.
### **`#DIV/0!`**
**Cause:** Division by zero or an empty cell.
**Fix:** Ensure the denominator isnt zero or blank. Use `IF` to handle such cases:
Division by zero or an empty cell.
```
=1/0
```
Usually this is an error. However, in cases where a denominator might be blank (e.g., data not yet filled in), this could be expected. Use `IFERROR` or `IF` to handle it.
```
=IF(B1=0, "N/A", A1/B1)
@@ -39,23 +47,42 @@ When working with formulas, you may encounter these common errors:
### **`#NAME?`**
**Cause:** Unrecognized text in the formula (e.g., misspelled function names or undefined named ranges).
**Fix:** Correct spelling or define the missing name.
Found when a name is not recognized. Maybe a misspeled name for a function. Could be a referenceto defined name that has been deleted.
```
=UNKOWN_FUNCTION(A1)
```
This indicates an error in your spreadsheet logic.
### **`#REF!`**
**Cause:** Invalid cell reference, often from deleting cells used in a formula.
**Fix:** Update the formula with correct references.
Indicates an invalid cell reference, often from deleting cells used in a formula.
They can appear as a result of a computation or in a formula. Examples:
```
=Sheet34!A1
```
If `Sheet34` doesn't exist it will return `#REF!`
This is a genuine error. It indicates that part of your formula references a cell or range that is missing.
### **`#NUM!`**
**Cause:** Invalid numeric operation (e.g., calculating a square root of a negative number).
**Fix:** Adjust the formula to ensure valid numeric operations.
Invalid numeric operation (e.g., calculating a square root of a negative number).
Adjust the formula to ensure valid numeric operations.
Sometimes a `#NUM!` might be expected signalling the user that some parameter is out of scope.
### **`#N/A`**
**Cause:** A value is not available, often in lookup functions like VLOOKUP.
**Fix:** Ensure the lookup value exists or use IFNA() to handle missing values:
A value is not available, often in lookup functions like VLOOKUP.
This is frequnly not an error in your spreadsheetlogic.
You can produce a prettier answer using the [`IFNA`](/functions/information/isna) formula:
```
=IFNA(VLOOKUP(A1, B1:C10, 2, FALSE), "Not Found")
@@ -63,17 +90,68 @@ When working with formulas, you may encounter these common errors:
### **`#NULL!`**
**Cause:** Incorrect range operator in a formula (e.g., missing a colon between cell references).
**Fix:** Use correct range operators (e.g., A1:A10).
Incorrect range operator in a formula (e.g., missing a colon between cell references).
### **`#SPILL!`**
A cell in a formula will overwrite content in other cells.
This cannot happen riht now in IronCalc as formulas don't spill yet.
### **`#CIRC!`**
**Cause:** Circular reference.
**Fix:** Remove the circular reference.
Circular reference. This is an error is your spreadsheet and must be fixed.
Means that during teh course of a computation a circular dependency was found.
A circular dependency is a dependency of a formula on itself.
For instance in the cell `A1` the formula `=A1*2` is a circular dependency.
Other spreadsheet engines use circular dependencies to do "loop computations", run "sensitivity analysis" or "goal seek".
IronCalc doesn't support any of those at the moment.
## IronCalc specific errors
---
### **`#ERROR!`**
General formula issue, like syntax errors or invalid references.
In general Excel does not let you enter incorrect formulas but IronCalc will.
This will make your workbook imcompatible with Excel
For instace an incomplete formula
```
=A1+
```
### **`#N/IMPL!`**
A particular feature is not yet implemented in IronCalc
Look if there is a Github ticket or contact us via email, Discord or bluesky
## Error propagation
Some errors a created by some formulas. For instance the function `SQRT` can create the error `#NUM!` but can't ceate the error `#DIV/0`.
Once an error is created it is normally _propagated_ by all the formulas. So if cell `C3` evaluates to `#ERROR!` then the formula
`=SQRT(C3)` will return `#ERROR!`.
Not all functions propagate errors in their arguments. For instancethe function `IF(condition, if_true, if_false)` will only propagate an error in the `if_false` argument if the `condition` is `FALSE`. This is called _lazy evaluation_, the function `IF` is _lazy_, it only evaluates the arguments when needed. The opposite of lazy evaulaution is called _eager evaluation_.
Some functions also expect an error as an argument like [`ERROR.TYPE`](/functions/information/error.type) and will not propagate the error.
### **`#####`**
## See also
**Cause:** The column isnt wide enough to display the value.
**Fix:** Resize the column width to fit the content.
The following functions are convenient when working with errors
- [`ISERR(ref)`](/functions/information/iserr), `TRUE` if `ref` is any error type except the `#N/A` error.
- [`ISERROR(ref)`](/functions/information/iserror), `TRUE` if `ref` is any error.
- [`ISNA(ref)`](/functions/information/isna), `TRUE` if ref is `#N/A`.
- [`ERROR.TYPE`](/functions/information/error.type) returns the numeric code for a given error.
- [`IFERROR(ref, value)`](/functions/logical/iferror) returns `value` if the content of `ref` is an error.
- [`IFNA(ref, value)`](/functions/logical/ifna) return `value` if `ref` is #N/A errors only.

View File

@@ -0,0 +1,15 @@
---
layout: doc
outline: deep
lang: en-US
---
# Implicit Intersection
::: warning
**Note:** This page is in construction 🚧
:::
::: warning
**Note:** This feature is not yet available on IronCalc 🚧
:::

View File

@@ -0,0 +1,26 @@
---
layout: doc
outline: deep
lang: en-US
---
# Error Types
::: warning
**Note:** This page is in construction 🚧
:::
Some functions have optional arguments. For instance:
XLOOKUP(lookup_value, lookup_array, return_array, if_not_found="#N/A", match_mode=0, search_mode=1)
The three first arguments are mandatory and the last three are optional.
Optional argumenst must have a default value.
In this example if you don't want to specify the if_not_found and match_mode arguments you can leave them out:
XLOOKUP(lookup_value, lookup_array, return_array, , , -2)
That would use the default arguments for if_not_found and match_mode arguments.

View File

@@ -0,0 +1,13 @@
---
layout: doc
outline: deep
lang: en-US
---
# Units
::: warning
**Note:** This page is in construction 🚧
:::
Some IronCalc functions return values that have units like currencies, percentage or dates.

View File

@@ -0,0 +1,67 @@
---
layout: doc
outline: deep
lang: en-US
---
# Value types
::: warning
**Note:** This page is in construction 🚧
:::
In IronCalc a value, a result of a calculation can be one of:
## Numbers
Numbers in IronCalc are [IEEE 754 double precission](https://en.wikipedia.org/wiki/Double-precision_floating-point_format).
Numbers are only displayed up to 15 significant figures. That's why '=0.1+0.2' is actually '0.3'
Also numbers are compared up to 15 significant figures. So `=IF(0.1+0.2=0.3, "Valid", "Invalid")` will return `Valid`.
However `=0.3-0.2-0.1` will not result in `0` in IronCalc.
### Casting into numbers
Strings and booleans are sometimes coverted to numbers
`=1+"2"` => 3
Some functions cast in weird ways:
SUM(1, TRUE) => 2
SUM(1, "1") => 2
But SUM(1, A1) => 1 (where A1 is TRUE or "1")
Sometimes the conversion happens like => "123"+1 is actually 124 and the SQRT("4") is 2 or the SQRT(TRUE) is 1.
Some functions, however are more strict BIN2DEC(TRUE) is #VALUE!
### Dates
On spreadsheets a date is just the number of days since January 1, 1900.
## Strings
### Complex numbers
On IronCal a complex number is just a string like "1+j3".
## Booleans
### Casting from numbers
## Errors
### Casting from strings
"#N/A" => #N/A
## Arrays

View File

@@ -13,37 +13,84 @@ FV can be used to calculate future value over a specified number of compounding
If your interest rate varies between periods, use the [FVSCHEDULE](/functions/financial/fvschedule) function instead of FV.
## Usage
### Syntax
**FV(rate, nper, pmt, pv, type)**
**FV(<span title="Number" style="color:#1E88E5">rate</span>, <span title="Number" style="color:#1E88E5">nper</span>, <span title="Number" style="color:#1E88E5">pmt</span>, <span title="Number" style="color:#1E88E5">pv</span>=0, <span title="Boolean" style="color:#43A047">type</span>=false) => <span title="Number" style="color:#1E88E5">fv</span>**
### Argument descriptions
* *rate*. The fixed percentage interest rate or yield per period.
* *nper*. The number of compounding periods to be taken into account. While this will often be an integer, non-integer values are accepted and processed.
* *pmt*. The fixed amount paid or deposited each compounding period.
* *pv* (optional). The present value or starting amount of the asset (default 0).
* *type* (optional). A logical value indicating whether the payment due dates are at the end (0) of the compounding periods or at the beginning (any non-zero value). The default is 0 when omitted.
* *rate*. ([number](/features/value-types)) The fixed percentage interest rate or yield per period.
* *nper*. ([number](/features/value-types)) The number of compounding periods to be taken into account. While this will often be an integer, non-integer values are accepted and processed.
It stands for <u>n</u>umber of <u>per</u>iods.
* *pmt*. ([number](/features/value-types)) The fixed amount paid or deposited each compounding period. Short for <u>p</u>ay<u>m</u>en<u>t</u>
* *pv* ([number](/features/value-types), optional). The present value or starting amount of the asset (default 0). Short for <u>p</u>resent <u>v</u>alue.
* *type* ([boolean](/features/value-types), optional). A logical value indicating whether the payment due dates are at the end (0) of the compounding periods or at the beginning (any non-zero value). The default is 0 when omitted.
### Returned value
The retruned value is a [number](/features/value-types) with [currency units](/features/units)
### Errors
* If any of the arguments is an error returns the error
* If any of the argumets is not a number (or cannot be converted to a number) it returns `#VALUE!`
* Some ranges of the parameters produce `#NUM!` error. For instnace `=FV(-3,1/2,1)`.
* Some ranges of the parameters produce the `#DIV/0!` error. For instance `=FV(-1, -1, 1)`
### Additional guidance
* Make sure that the *rate* argument specifies the interest rate or yield applicable to the compounding period, based on the value chosen for *nper*.
* The *pmt* and *pv* arguments should be expressed in the same currency unit. The value returned is expressed in the same currency unit.
* To ensure a worthwhile result, one of the *pmt* and *pv* arguments should be non-zero.
* The setting of the *type* argument only affects the calculation for non-zero values of the *pmt* argument.
<!--@include: ../markdown-snippets/error-type-details.md-->
<!--@include: ../markdown-snippets/error-type-details.txt-->
## Details
* If *rate* = 0, FV is given by the equation:
* If $\text{type} \neq 0$, $\text{fv}$ is given by the equation:
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^\text{nper} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big) \times(1+\text{rate})}{\text{rate}}$$
* If $\text{type} = 0$
$$ \text{fv} = -\text{pv} \times (1 + \text{rate})^{\text{nper}} - \dfrac{\text{pmt}\times\big({(1+\text{rate})^\text{nper}-1}\big)}{\text{rate}}$$
* In both cases, in the limmit $\text{rate} = 0$, fv is given by the equation:
$$ \text{fv} = -\text{pv} - (\text{pmt} \times \text{nper}) $$
## Formula derivation
The money you have now might grow in a bank by _[compound interest](https://en.wikipedia.org/wiki/Compound_interest)_. Say you have $100, that is the present value, and your bank gives you 10% interest rate yearly.
At the end of 1 year you will have $110. In general that is $\text{pv}\times (1 + \text{rate})$. At the end of two years (the second _[annuity](https://en.wikipedia.org/wiki/Annuity)_) you will have 10\% more of the $110. That is the _compound_ part. You will have at the end of the second period $121 or in general if you invest an ammount $\text{pv}$ at an interest $\text{rate}$ and wait for $\text{nper}$ periods the future value of this _[lump sum](https://en.wikipedia.org/wiki/Lump_sum)_ will be:
$$\text{fv}_\text{ls} = \text{pv} \times (1 + \text{rate})^\text{nper}$$
Note that the periods may be years, months or anything else.
Now, supose that you also make regular payments of ammount $\text{pmt}$ each period.
To find the future value of these payments, you sum the future value of each payment at the end of the investment horizon.
There are two posibilities here:
* You make the payments at the end of the periods (type 0). This is also called an _ordinary annuity_.
* You make the payments at the beginning of each period (type 1). This is the _annuity due_ case.
To derive the formula for either of them we need to add an geometric progression. To simplify things.
Say we are at the end of period 5 and we are making the payments at the end of the period.
$$
FV = -pv - (pmt \times nper)
\text{pmt}\times (1 + \text{rate})^4+\text{pmt}\times (1 + \text{rate})^3+\text{pmt}\times (1 + \text{rate})^2 +\text{pmt}
$$
* If *rate* <> 0 and *type* = 0, FV is given by the equation:
$$ FV = -pv \times (1 + rate)^{nper} - \dfrac{pmt\times\big({(1+rate)^{nper}-1}\big)}{rate}
This is because the first payment has been around for 4 periods, and the second payment has been around for 3 periods...
The general formula for the sum of $\text{nper}$ terms in a geometric progression is given by $a(1 - r^n) / (1 - r)$, where $a = \text{pmt}$ and $r = 1 + \text{rate}$
$$
* If *rate* <> 0 and *type* <> 0, FV is given by the equation:
$$ FV = -pv \times (1 + rate)^{nper} - \dfrac{pmt\times\big({(1+rate)^{nper}-1}\big) \times(1+rate)}{rate}
\text{pmt}\times\dfrac{ (1+\text{rate})^{\text{nper}}-1}{\text{rate}}
$$
## Examples
[See this example in IronCalc](https://app.ironcalc.com/?example=fv).
## Links
* For more information about the concept of "future value" in finance, visit Wikipedia's [Future value](https://en.wikipedia.org/wiki/Future_value) page.
* [Investorpedia](https://www.investopedia.com/terms/f/futurevalue.asp) has a nice article on the future value.
* See also IronCalc's [NPER](/functions/financial/nper), [PMT](/functions/financial/pmt), [PV](/functions/financial/pv) and [RATE](/functions/financial/rate) functions.
* Visit Microsoft Excel's [FV function](https://support.microsoft.com/en-gb/office/fv-function-2eef9f44-a084-4c61-bdd8-4fe4bb1b71b3) page.
* Both [Google Sheets](https://support.google.com/docs/answer/3093224) and [LibreOffice Calc](https://wiki.documentfoundation.org/Documentation/Calc_Functions/FV) provide versions of the FV function.

View File

@@ -1,160 +0,0 @@
import styled from "@emotion/styled";
import { Trash2 } from "lucide-react";
import { forwardRef, useEffect } from "react";
import { theme } from "../theme";
export const DeleteWorkbookDialog = forwardRef<
HTMLDivElement,
{
onClose: () => void;
onConfirm: () => void;
workbookName: string;
}
>((properties, ref) => {
useEffect(() => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "blur(2px)";
}
return () => {
const root = document.getElementById("root");
if (root) {
root.style.filter = "none";
}
};
}, []);
return (
<DialogWrapper
ref={ref}
tabIndex={-1}
role="dialog"
aria-labelledby="delete-dialog-title"
aria-describedby="delete-dialog-description"
>
<IconWrapper>
<Trash2 />
</IconWrapper>
<ContentWrapper>
<Title>Are you sure?</Title>
<Body>
The workbook <strong>'{properties.workbookName}'</strong> will be
permanently deleted. This action cannot be undone.
</Body>
<ButtonGroup>
<DeleteButton
onClick={() => {
properties.onConfirm();
properties.onClose();
}}
>
Yes, delete workbook
</DeleteButton>
<CancelButton onClick={properties.onClose}>Cancel</CancelButton>
</ButtonGroup>
</ContentWrapper>
</DialogWrapper>
);
});
DeleteWorkbookDialog.displayName = "DeleteWorkbookDialog";
const DialogWrapper = styled.div`
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
display: flex;
flex-direction: column;
gap: 16px;
padding: 12px;
border-radius: 8px;
box-shadow: 0px 1px 3px 0px ${theme.palette.common.black}1A;
width: 280px;
max-width: calc(100% - 40px);
z-index: 50;
font-family: "Inter", sans-serif;
`;
const IconWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
border-radius: 4px;
background-color: ${theme.palette.error.main}1A;
margin: 12px auto 0 auto;
color: ${theme.palette.error.main};
svg {
width: 16px;
height: 16px;
}
`;
const ContentWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
font-size: 14px;
word-break: break-word;
`;
const Title = styled.h2`
margin: 0;
font-weight: 600;
font-size: inherit;
color: ${theme.palette.grey["900"]};
`;
const Body = styled.p`
margin: 0;
text-align: center;
color: ${theme.palette.grey["900"]};
font-size: 12px;
`;
const ButtonGroup = styled.div`
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
width: 100%;
`;
const Button = styled.button`
cursor: pointer;
color: ${theme.palette.common.white};
background-color: ${theme.palette.primary.main};
padding: 0px 10px;
height: 36px;
border-radius: 4px;
border: none;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
text-overflow: ellipsis;
transition: background-color 150ms;
&:hover {
background-color: ${theme.palette.primary.dark};
}
`;
const DeleteButton = styled(Button)`
background-color: ${theme.palette.error.main};
color: ${theme.palette.common.white};
&:hover {
background-color: ${theme.palette.error.dark};
}
`;
const CancelButton = styled(Button)`
background-color: ${theme.palette.grey["200"]};
color: ${theme.palette.grey["700"]};
&:hover {
background-color: ${theme.palette.grey["300"]};
}
`;

View File

@@ -2,7 +2,6 @@ import styled from "@emotion/styled";
import { Menu, MenuItem, Modal } from "@mui/material";
import { Check, FileDown, FileUp, Plus, Trash2 } from "lucide-react";
import { useRef, useState } from "react";
import { DeleteWorkbookDialog } from "./DeleteWorkbookDialog";
import { UploadFileDialog } from "./UploadFileDialog";
import { getModelsMetadata, getSelectedUuid } from "./storage";
@@ -19,7 +18,6 @@ export function FileMenu(props: {
const models = getModelsMetadata();
const uuids = Object.keys(models);
const selectedUuid = getSelectedUuid();
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
const elements = [];
for (const uuid of uuids) {
@@ -90,14 +88,16 @@ export function FileMenu(props: {
Download (.xlsx)
</MenuItemText>
</MenuItemWrapper>
<MenuItemWrapper
onClick={() => {
setDeleteDialogOpen(true);
setMenuOpen(false);
}}
>
<MenuItemWrapper>
<StyledTrash />
<MenuItemText>Delete workbook</MenuItemText>
<MenuItemText
onClick={() => {
props.onDelete();
setMenuOpen(false);
}}
>
Delete workbook
</MenuItemText>
</MenuItemWrapper>
<MenuDivider />
{elements}
@@ -127,18 +127,6 @@ export function FileMenu(props: {
/>
</>
</Modal>
<Modal
open={isDeleteDialogOpen}
onClose={() => setDeleteDialogOpen(false)}
aria-labelledby="delete-dialog-title"
aria-describedby="delete-dialog-description"
>
<DeleteWorkbookDialog
onClose={() => setDeleteDialogOpen(false)}
onConfirm={props.onDelete}
workbookName={selectedUuid ? models[selectedUuid] : ""}
/>
</Modal>
</>
);
}

View File

@@ -1,99 +0,0 @@
import { styled } from "@mui/material";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { Check } from "lucide-react";
import type { SheetOptions } from "./types";
function isWhiteColor(color: string): boolean {
return ["#FFF", "#FFFFFF"].includes(color);
}
interface SheetListMenuProps {
open: boolean;
onClose: () => void;
anchorEl: HTMLButtonElement | null;
onSheetSelected: (index: number) => void;
sheetOptionsList: SheetOptions[];
selectedIndex: number;
}
const SheetListMenu = (properties: SheetListMenuProps) => {
const {
open,
onClose,
anchorEl,
onSheetSelected,
sheetOptionsList,
selectedIndex,
} = properties;
const hasColors = sheetOptionsList.some((tab) => !isWhiteColor(tab.color));
return (
<StyledMenu
open={open}
onClose={onClose}
anchorEl={anchorEl}
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
transformOrigin={{
vertical: "bottom",
horizontal: 6,
}}
>
{sheetOptionsList.map((tab, index) => (
<StyledMenuItem
key={tab.sheetId}
onClick={() => onSheetSelected(index)}
>
{index === selectedIndex ? (
<Check
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
) : (
<div
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
)}
{hasColors && <ItemColor style={{ backgroundColor: tab.color }} />}
<ItemName
style={{ fontWeight: index === selectedIndex ? "bold" : "normal" }}
>
{tab.name}
</ItemName>
</StyledMenuItem>
))}
</StyledMenu>
);
};
const StyledMenu = styled(Menu)({
"& .MuiPaper-root": {
borderRadius: 8,
padding: 4,
},
"& .MuiList-padding": {
padding: 0,
},
});
const StyledMenuItem = styled(MenuItem)({
padding: 8,
borderRadius: 4,
});
const ItemColor = styled("div")`
width: 12px;
height: 12px;
border-radius: 4px;
margin-right: 8px;
`;
const ItemName = styled("div")`
font-size: 12px;
color: #333;
`;
export default SheetListMenu;

View File

@@ -1 +0,0 @@
export { default } from "./SheetTabBar";

View File

@@ -5,10 +5,10 @@ export const headerGlobalSelectorColor = "#EAECF4";
export const headerSelectedBackground = "#EEEEEE";
export const headerFullSelectedBackground = "#D3D6E9";
export const headerSelectedColor = "#333";
export const headerBorderColor = "#E0E0E0";
export const headerBorderColor = "#DEE0EF";
export const gridColor = "#E0E0E0";
export const gridSeparatorColor = "#E0E0E0";
export const gridSeparatorColor = "#D3D6E9";
export const defaultTextColor = "#2E414D";
export const outlineColor = "#F2994A";

View File

@@ -38,7 +38,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
const { t } = useTranslation();
const [borderSelected, setBorderSelected] = useState<BorderType | null>(null);
const [borderColor, setBorderColor] = useState(theme.palette.common.white);
const [borderColor, setBorderColor] = useState("#000000");
const [borderStyle, setBorderStyle] = useState(BorderStyle.Thin);
const [colorPickerOpen, setColorPickerOpen] = useState(false);
const [stylePickerOpen, setStylePickerOpen] = useState(false);
@@ -62,7 +62,7 @@ const BorderPicker = (properties: BorderPickerProps) => {
// biome-ignore lint/correctness/useExhaustiveDependencies: We reset the styles, every time we open (or close) the widget
useEffect(() => {
setBorderSelected(null);
setBorderColor(theme.palette.common.white);
setBorderColor("#000000");
setBorderStyle(BorderStyle.Thin);
}, [properties.open]);
@@ -240,21 +240,31 @@ const BorderPicker = (properties: BorderPickerProps) => {
</Borders>
<Divider />
<Styles>
<ButtonWrapper
onClick={() => setColorPickerOpen(true)}
ref={borderColorButton}
>
<PencilLine />
<ButtonWrapper onClick={() => setColorPickerOpen(true)}>
<Button
type="button"
$pressed={false}
disabled={false}
ref={borderColorButton}
title={t("toolbar.borders.color")}
>
<PencilLine />
</Button>
<div style={{ flexGrow: 2 }}>Border color</div>
<ChevronRightStyled />
</ButtonWrapper>
<ButtonWrapper
onClick={() => setStylePickerOpen(true)}
ref={borderStyleButton}
>
<BorderStyleIcon />
<Button
type="button"
$pressed={false}
disabled={false}
title={t("toolbar.borders.style")}
>
<BorderStyleIcon />
</Button>
<div style={{ flexGrow: 2 }}>Border style</div>
<ChevronRightStyled />
</ButtonWrapper>
@@ -271,14 +281,6 @@ const BorderPicker = (properties: BorderPickerProps) => {
}}
anchorEl={borderColorButton}
open={colorPickerOpen}
anchorOrigin={{
vertical: "top", // Keep vertical alignment at the top
horizontal: "right", // Set horizontal alignment to right
}}
transformOrigin={{
vertical: "top", // Keep vertical alignment at the top
horizontal: "left", // Set horizontal alignment to left
}}
/>
<StyledPopover
open={stylePickerOpen}
@@ -286,10 +288,8 @@ const BorderPicker = (properties: BorderPickerProps) => {
setStylePickerOpen(false);
}}
anchorEl={borderStyleButton.current}
anchorOrigin={{
vertical: "top",
horizontal: "right",
}}
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
transformOrigin={{ vertical: 38, horizontal: -6 }}
>
<BorderStyleDialog>
<LineWrapper
@@ -336,12 +336,12 @@ const LineWrapper = styled("div")<LineWrapperProperties>`
align-items: center;
background-color: ${({ $checked }): string => {
if ($checked) {
return theme.palette.grey["200"];
return "#EEEEEE;";
}
return "inherit;";
}};
&:hover {
border: 1px solid ${theme.palette.grey["200"]};
border: 1px solid #eeeeee;
}
padding: 8px;
cursor: pointer;
@@ -351,59 +351,52 @@ const LineWrapper = styled("div")<LineWrapperProperties>`
const SolidLine = styled("div")`
width: 68px;
border-top: 1px solid ${theme.palette.grey["900"]};
border-top: 1px solid #333333;
`;
const MediumLine = styled("div")`
width: 68px;
border-top: 2px solid ${theme.palette.grey["900"]};
border-top: 2px solid #333333;
`;
const ThickLine = styled("div")`
width: 68px;
border-top: 1px solid ${theme.palette.grey["900"]};
border-top: 3px solid #333333;
`;
const Divider = styled("div")`
width: 100%;
margin: auto;
border-top: 1px solid ${theme.palette.grey["200"]};
display: inline-flex;
heigh: 1px;
border-bottom: 1px solid #eee;
margin-left: 0px;
margin-right: 0px;
`;
const Borders = styled("div")`
display: flex;
flex-direction: column;
gap: 4px;
padding: 4px;
padding-bottom: 4px;
`;
const Styles = styled("div")`
display: flex;
flex-direction: column;
padding: 4px;
`;
const Line = styled("div")`
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
`;
const ButtonWrapper = styled("div")`
display: flex;
flex-direction: row;
align-items: center;
border-radius: 4px;
gap: 8px;
&:hover {
background-color: ${theme.palette.grey["200"]};
border-top-color: ${(): string => theme.palette.grey["200"]};
background-color: #eee;
border-top-color: ${(): string => theme.palette.grey["400"]};
}
cursor: pointer;
padding: 8px;
svg {
width: 16px;
height: 16px;
}
`;
const BorderStyleDialog = styled("div")`
@@ -416,7 +409,7 @@ const BorderStyleDialog = styled("div")`
const StyledPopover = styled(Popover)`
.MuiPopover-paper {
border-radius: 8px;
border-radius: 10px;
border: 0px solid ${({ theme }): string => theme.palette.background.default};
box-shadow: 1px 2px 8px rgba(139, 143, 173, 0.5);
}
@@ -432,6 +425,7 @@ const StyledPopover = styled(Popover)`
const BorderPickerDialog = styled("div")`
background: ${({ theme }): string => theme.palette.background.default};
padding: 4px;
display: flex;
flex-direction: column;
`;
@@ -450,8 +444,10 @@ const Button = styled("button")<TypeButtonProperties>(
alignItems: "center",
justifyContent: "center",
// fontSize: "26px",
border: `0px solid ${theme.palette.common.white}`,
border: "0px solid #fff",
borderRadius: "4px",
marginRight: "5px",
transition: "all 0.2s",
cursor: "pointer",
padding: "0px",
};
@@ -464,15 +460,13 @@ const Button = styled("button")<TypeButtonProperties>(
}
return {
...result,
borderTop: $underlinedColor
? `3px solid ${theme.palette.common.white}`
: "none",
borderTop: $underlinedColor ? "3px solid #FFF" : "none",
borderBottom: $underlinedColor ? `3px solid ${$underlinedColor}` : "none",
color: `${theme.palette.grey["900"]}`,
color: "#21243A",
backgroundColor: $pressed ? theme.palette.grey["200"] : "inherit",
"&:hover": {
outline: `1px solid ${theme.palette.grey["200"]}`,
borderTopColor: theme.palette.grey["200"],
backgroundColor: "#F1F2F8",
borderTopColor: "#F1F2F8",
},
svg: {
width: "16px",

View File

@@ -45,8 +45,8 @@ const ColorPicker = (properties: ColorPickerProps) => {
"#3BB68A",
"#8CB354",
"#F8CD3C",
"#F2994A",
"#EC5753",
"#A23C52",
"#D03627",
"#523E93",
"#3358B7",

View File

@@ -0,0 +1,2 @@
export { default } from "./navigation";
export type { NavigationProps } from "./navigation";

View File

@@ -1,23 +1,31 @@
import { Dialog, TextField, styled } from "@mui/material";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import { Check } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { theme } from "../../theme";
import type { SheetOptions } from "./types";
function isWhiteColor(color: string): boolean {
return ["#FFF", "#FFFFFF"].includes(color);
}
interface SheetRenameDialogProps {
open: boolean;
onClose: () => void;
isOpen: boolean;
close: () => void;
onNameChanged: (name: string) => void;
defaultName: string;
}
const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
export const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
const { t } = useTranslation();
const [name, setName] = useState(properties.defaultName);
const handleClose = () => {
properties.onClose();
properties.close();
};
return (
<Dialog open={properties.open} onClose={properties.onClose}>
<Dialog open={properties.isOpen} onClose={properties.close}>
<StyledDialogTitle>
{t("sheet_rename.title")}
<Cross onClick={handleClose} onKeyDown={() => {}}>
@@ -53,9 +61,7 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
event.stopPropagation();
if (event.key === "Enter") {
properties.onNameChanged(name);
properties.onClose();
} else if (event.key === "Escape") {
properties.onClose();
properties.close();
}
}}
onChange={(event) => {
@@ -63,8 +69,6 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
}}
spellCheck="false"
onPaste={(event) => event.stopPropagation()}
onCopy={(event) => event.stopPropagation()}
onCut={(event) => event.stopPropagation()}
/>
</StyledDialogContent>
<DialogFooter>
@@ -80,6 +84,94 @@ const SheetRenameDialog = (properties: SheetRenameDialogProps) => {
);
};
interface SheetListMenuProps {
isOpen: boolean;
close: () => void;
anchorEl: HTMLButtonElement | null;
onSheetSelected: (index: number) => void;
sheetOptionsList: SheetOptions[];
selectedIndex: number;
}
const SheetListMenu = (properties: SheetListMenuProps) => {
const {
isOpen,
close,
anchorEl,
onSheetSelected,
sheetOptionsList,
selectedIndex,
} = properties;
const hasColors = sheetOptionsList.some((tab) => !isWhiteColor(tab.color));
return (
<StyledMenu
open={isOpen}
onClose={close}
anchorEl={anchorEl}
anchorOrigin={{
vertical: "top",
horizontal: "left",
}}
transformOrigin={{
vertical: "bottom",
horizontal: 6,
}}
>
{sheetOptionsList.map((tab, index) => (
<StyledMenuItem
key={tab.sheetId}
onClick={() => onSheetSelected(index)}
>
{index === selectedIndex ? (
<Check
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
) : (
<div
style={{ width: "16px", height: "16px", marginRight: "8px" }}
/>
)}
{hasColors && <ItemColor style={{ backgroundColor: tab.color }} />}
<ItemName
style={{ fontWeight: index === selectedIndex ? "bold" : "normal" }}
>
{tab.name}
</ItemName>
</StyledMenuItem>
))}
</StyledMenu>
);
};
const StyledMenu = styled(Menu)({
"& .MuiPaper-root": {
borderRadius: 8,
padding: 4,
},
"& .MuiList-padding": {
padding: 0,
},
});
const StyledMenuItem = styled(MenuItem)({
padding: 8,
borderRadius: 4,
});
const ItemColor = styled("div")`
width: 12px;
height: 12px;
border-radius: 4px;
margin-right: 8px;
`;
const ItemName = styled("div")`
font-size: 12px;
color: #333;
`;
const StyledDialogTitle = styled("div")`
display: flex;
align-items: center;
@@ -154,4 +246,4 @@ const StyledButton = styled("div")`
}
`;
export default SheetRenameDialog;
export default SheetListMenu;

View File

@@ -6,11 +6,11 @@ import { theme } from "../../theme";
import { NAVIGATION_HEIGHT } from "../constants";
import { StyledButton } from "../toolbar";
import type { WorkbookState } from "../workbookState";
import SheetListMenu from "./SheetListMenu";
import SheetTab from "./SheetTab";
import SheetListMenu from "./menus";
import Sheet from "./sheet";
import type { SheetOptions } from "./types";
export interface SheetTabBarProps {
export interface NavigationProps {
sheets: SheetOptions[];
selectedIndex: number;
workbookState: WorkbookState;
@@ -21,7 +21,7 @@ export interface SheetTabBarProps {
onSheetDeleted: () => void;
}
function SheetTabBar(props: SheetTabBarProps) {
function Navigation(props: NavigationProps) {
const { t } = useTranslation();
const { workbookState, onSheetSelected, sheets, selectedIndex } = props;
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
@@ -35,27 +35,24 @@ function SheetTabBar(props: SheetTabBarProps) {
return (
<Container>
<LeftButtonsContainer>
<StyledButton
title={t("navigation.add_sheet")}
$pressed={false}
onClick={props.onAddBlankSheet}
>
<Plus />
</StyledButton>
<StyledButton
onClick={handleClick}
title={t("navigation.sheet_list")}
$pressed={false}
>
<Menu />
</StyledButton>
</LeftButtonsContainer>
<VerticalDivider />
<StyledButton
title={t("navigation.add_sheet")}
$pressed={false}
onClick={props.onAddBlankSheet}
>
<Plus />
</StyledButton>
<StyledButton
onClick={handleClick}
title={t("navigation.sheet_list")}
$pressed={false}
>
<Menu />
</StyledButton>
<Sheets>
<SheetInner>
{sheets.map((tab, index) => (
<SheetTab
<Sheet
key={tab.sheetId}
name={tab.name}
color={tab.color}
@@ -71,7 +68,6 @@ function SheetTabBar(props: SheetTabBarProps) {
props.onSheetDeleted();
}}
workbookState={workbookState}
canDelete={sheets.length > 1}
/>
))}
</SheetInner>
@@ -81,8 +77,8 @@ function SheetTabBar(props: SheetTabBarProps) {
</Advert>
<SheetListMenu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
isOpen={open}
close={handleClose}
sheetOptionsList={sheets}
onSheetSelected={(index) => {
onSheetSelected(index);
@@ -96,8 +92,6 @@ function SheetTabBar(props: SheetTabBarProps) {
// Note I have to specify the font-family in every component that can be considered stand-alone
const Container = styled("div")`
display: flex;
flex-direction: row;
position: absolute;
bottom: 0px;
left: 0px;
@@ -105,10 +99,10 @@ const Container = styled("div")`
display: flex;
height: ${NAVIGATION_HEIGHT}px;
align-items: center;
padding: 0px 12px;
padding-left: 12px;
font-family: Inter;
background-color: ${theme.palette.common.white};
border-top: 1px solid ${theme.palette.grey["300"]};
background-color: #fff;
border-top: 1px solid #e0e0e0;
`;
const Sheets = styled("div")`
@@ -116,9 +110,6 @@ const Sheets = styled("div")`
overflow: hidden;
overflow-x: auto;
scrollbar-width: none;
padding-left: 12px;
display: flex;
flex-direction: row;
`;
const SheetInner = styled("div")`
@@ -128,8 +119,8 @@ const SheetInner = styled("div")`
const Advert = styled("a")`
display: flex;
align-items: center;
color: ${theme.palette.primary.main};
padding: 0px 0px 0px 12px;
color: #f2994a;
padding: 0px 12px;
font-size: 12px;
text-decoration: none;
border-left: 1px solid ${theme.palette.grey["300"]};
@@ -142,19 +133,4 @@ const Advert = styled("a")`
}
`;
const LeftButtonsContainer = styled("div")`
display: flex;
flex-direction: row;
gap: 4px;
padding-right: 12px;
`;
const VerticalDivider = styled("div")`
height: 100%;
width: 0px;
@media (max-width: 769px) {
border-right: 1px solid ${theme.palette.grey["200"]};
}
`;
export default SheetTabBar;
export default Navigation;

View File

@@ -1,26 +1,23 @@
import { Button, Menu, MenuItem, styled } from "@mui/material";
import type { MenuItemProps } from "@mui/material";
import { ChevronDown } from "lucide-react";
import { useRef, useState } from "react";
import { theme } from "../../theme";
import ColorPicker from "../colorPicker";
import { isInReferenceMode } from "../editor/util";
import type { WorkbookState } from "../workbookState";
import SheetRenameDialog from "./SheetRenameDialog";
import { SheetRenameDialog } from "./menus";
interface SheetTabProps {
interface SheetProps {
name: string;
color: string;
selected: boolean;
onSelected: () => void;
onColorChanged: (hex: string) => void;
onRenamed: (name: string) => void;
canDelete: boolean;
onDeleted: () => void;
workbookState: WorkbookState;
}
function SheetTab(props: SheetTabProps) {
function Sheet(props: SheetProps) {
const { name, color, selected, workbookState, onSelected } = props;
const [anchorEl, setAnchorEl] = useState<null | HTMLButtonElement>(null);
const [colorPickerOpen, setColorPickerOpen] = useState(false);
@@ -41,9 +38,8 @@ function SheetTab(props: SheetTabProps) {
};
return (
<>
<TabWrapper
$color={color}
$selected={selected}
<Wrapper
style={{ borderBottomColor: color, fontWeight: selected ? 600 : 400 }}
onClick={(event) => {
onSelected();
event.stopPropagation();
@@ -59,11 +55,11 @@ function SheetTab(props: SheetTabProps) {
}}
ref={colorButton}
>
<Name onDoubleClick={handleOpenRenameDialog}>{name}</Name>
<Name>{name}</Name>
<StyledButton onClick={handleOpen}>
<ChevronDown />
</StyledButton>
</TabWrapper>
</Wrapper>
<StyledMenu
anchorEl={anchorEl}
open={open}
@@ -94,7 +90,6 @@ function SheetTab(props: SheetTabProps) {
Change Color
</StyledMenuItem>
<StyledMenuItem
disabled={!props.canDelete}
onClick={() => {
props.onDeleted();
handleClose();
@@ -105,8 +100,8 @@ function SheetTab(props: SheetTabProps) {
</StyledMenuItem>
</StyledMenu>
<SheetRenameDialog
open={renameDialogOpen}
onClose={handleCloseRenameDialog}
isOpen={renameDialogOpen}
close={handleCloseRenameDialog}
defaultName={name}
onNameChanged={(newName) => {
props.onRenamed(newName);
@@ -129,42 +124,10 @@ function SheetTab(props: SheetTabProps) {
);
}
const StyledMenu = styled(Menu)`
& .MuiPaper-root {
border-radius: 8px;
padding: 4px 0px;
margin-left: -4px;
}
& .MuiList-root {
padding: 0;
}
`;
const StyledMenu = styled(Menu)``;
const StyledMenuItem = styled(MenuItem)<MenuItemProps>(() => ({
display: "flex",
justifyContent: "space-between",
fontSize: "12px",
width: "calc(100% - 8px)",
margin: "0px 4px",
borderRadius: "4px",
padding: "8px",
height: "32px",
"&:disabled": {
color: "#BDBDBD",
},
}));
const TabWrapper = styled("div")<{ $color: string; $selected: boolean }>`
display: flex;
margin-right: 12px;
border-bottom: 3px solid ${(props) => props.$color};
line-height: 37px;
padding: 0px 4px;
align-items: center;
cursor: pointer;
font-weight: ${(props) => (props.$selected ? 600 : 400)};
background-color: ${(props) =>
props.$selected ? `${theme.palette.grey[50]}80` : "transparent"};
const StyledMenuItem = styled(MenuItem)`
font-size: 12px;
`;
const StyledButton = styled(Button)`
@@ -174,27 +137,26 @@ const StyledButton = styled(Button)`
padding: 0px;
color: inherit;
font-weight: inherit;
&:hover {
background-color: transparent;
}
&:active {
background-color: transparent;
}
svg {
width: 15px;
height: 15px;
transition: transform 0.2s;
}
&:hover svg {
transform: translateY(2px);
}
`;
const Wrapper = styled("div")`
display: flex;
margin-left: 20px;
border-bottom: 3px solid;
border-top: 3px solid white;
line-height: 34px;
align-items: center;
cursor: pointer;
`;
const Name = styled("div")`
font-size: 12px;
margin-right: 5px;
text-wrap: nowrap;
user-select: none;
`;
export default SheetTab;
export default Sheet;

View File

@@ -18,7 +18,7 @@ import {
Grid2x2X,
Italic,
PaintBucket,
PaintRoller,
Paintbrush2,
Percent,
Redo2,
Strikethrough,
@@ -114,7 +114,7 @@ function Toolbar(properties: ToolbarProperties) {
onClick={properties.onCopyStyles}
title={t("toolbar.copy_styles")}
>
<PaintRoller />
<Paintbrush2 />
</StyledButton>
<Divider />
<StyledButton
@@ -183,7 +183,8 @@ function Toolbar(properties: ToolbarProperties) {
title={t("toolbar.format_number")}
sx={{
width: "40px", // Keep in sync with anchorOrigin in FormatMenu above
padding: "0px 4px",
fontSize: "13px",
fontWeight: 400,
}}
>
{"123"}
@@ -390,9 +391,7 @@ const ToolbarContainer = styled("div")`
font-family: Inter;
border-radius: 4px 4px 0px 0px;
overflow-x: auto;
padding: 0px 12px;
gap: 4px;
scrollbar-width: none;
padding-left: 11px;
`;
type TypeButtonProperties = { $pressed: boolean; $underlinedColor?: string };
@@ -400,16 +399,15 @@ export const StyledButton = styled("button")<TypeButtonProperties>(
({ disabled, $pressed, $underlinedColor }) => {
const result = {
width: "24px",
minWidth: "24px",
height: "24px",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
fontSize: "12px",
border: `0px solid ${theme.palette.common.white}`,
borderRadius: "4px",
fontSize: "26px",
border: "0px solid #fff",
borderRadius: "2px",
marginRight: "5px",
transition: "all 0.2s",
outline: `1px solid ${theme.palette.common.white}`,
cursor: "pointer",
backgroundColor: "white",
padding: "0px",
@@ -421,28 +419,19 @@ export const StyledButton = styled("button")<TypeButtonProperties>(
if (disabled) {
return {
...result,
color: theme.palette.grey["400"],
color: theme.palette.grey["600"],
cursor: "default",
};
}
return {
...result,
borderTop: $underlinedColor
? `3px solid ${theme.palette.common.white}`
: "none",
borderTop: $underlinedColor ? "3px solid #FFF" : "none",
borderBottom: $underlinedColor ? `3px solid ${$underlinedColor}` : "none",
color: theme.palette.grey["900"],
backgroundColor: $pressed
? theme.palette.grey["300"]
: theme.palette.common.white,
color: "#21243A",
backgroundColor: $pressed ? "#EEE" : "#FFF",
"&:hover": {
transition: "all 0.2s",
outline: `1px solid ${theme.palette.grey["200"]}`,
borderTopColor: theme.palette.common.white,
},
"&:active": {
backgroundColor: theme.palette.grey["300"],
outline: `1px solid ${theme.palette.grey["300"]}`,
backgroundColor: "#F1F2F8",
borderTopColor: "#F1F2F8",
},
};
},
@@ -450,9 +439,10 @@ export const StyledButton = styled("button")<TypeButtonProperties>(
const Divider = styled("div")({
width: "0px",
height: "12px",
borderLeft: `1px solid ${theme.palette.grey["300"]}`,
margin: "0px 12px",
height: "10px",
borderLeft: "1px solid #E0E0E0",
marginLeft: "5px",
marginRight: "10px",
});
export default Toolbar;

View File

@@ -6,7 +6,6 @@ import type {
} from "@ironcalc/wasm";
import { styled } from "@mui/material/styles";
import { useCallback, useEffect, useRef, useState } from "react";
import SheetTabBar from "./SheetTabBar/SheetTabBar";
import {
COLUMN_WIDTH_SCALE,
LAST_COLUMN,
@@ -17,6 +16,7 @@ import {
getNewClipboardId,
} from "./clipboard";
import FormulaBar from "./formulabar";
import Navigation from "./navigation/navigation";
import Toolbar from "./toolbar";
import useKeyboardNavigation from "./useKeyboardNavigation";
import { type NavigationKey, getCellAddress } from "./util";
@@ -390,12 +390,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}
data.set(Number.parseInt(row, 10), rowMap);
}
model.pasteFromClipboard(
source.sheet,
source.area,
data,
source.type === "cut",
);
model.pasteFromClipboard(source.area, data, source.type === "cut");
setRedrawId((id) => id + 1);
} else if (mimeType === "text/plain") {
const {
@@ -421,7 +416,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}}
onCopy={(event: React.ClipboardEvent) => {
const data = model.copyToClipboard();
const sheet = model.getSelectedSheet();
// '2024-10-18T14:07:37.599Z'
let clipboardId = sessionStorage.getItem(
@@ -449,7 +443,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
type: "copy",
area: data.range,
sheetData,
sheet,
clipboardId,
});
event.clipboardData.setData("text/plain", data.csv);
@@ -459,7 +452,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}}
onCut={(event: React.ClipboardEvent) => {
const data = model.copyToClipboard();
const sheet = model.getSelectedSheet();
// '2024-10-18T14:07:37.599Z'
let clipboardId = sessionStorage.getItem(
@@ -487,7 +479,6 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
type: "cut",
area: data.range,
sheetData,
sheet,
clipboardId,
});
event.clipboardData.setData("text/plain", data.csv);
@@ -581,7 +572,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
}}
/>
<SheetTabBar
<Navigation
sheets={info}
selectedIndex={model.getSelectedSheet()}
workbookState={workbookState}

View File

@@ -1,3 +1,7 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8H2M14 8H13M7 8H5M11 8H9M14 4H2M2.01 12H2M4.01 12H4M6.01 12H6M8.01 12H8M10.01 12H10M12.01 12H12M14.01 12H14" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="0" y1="2" x2="16" y2="2" stroke="#000"/>
<!-- Dashes and gaps of the same size -->
<line x1="0" y1="8" x2="16" y2="8" stroke-dasharray="2.28 2.28" stroke="#000"/>
<!-- Dashes and gaps of different sizes -->
<line x1="0" y1="14" x2="16" y2="14" stroke-dasharray="1 2" stroke="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 290 B

After

Width:  |  Height:  |  Size: 400 B