Compare commits

..

10 Commits

Author SHA1 Message Date
Nicolás Hatcher
379c84f64a WIP: Failed attempt to do a solid-js app.
For now...
2024-05-22 20:55:54 +02:00
Nicolás Hatcher Andrés
49c3b14bf0 UPDATE: Adds get/set views to the user model API (#69) 2024-05-20 17:51:09 +02:00
Nicolás Hatcher Andrés
d2cba48f8e FIX: Fixes incorrect result in M1 Apple silicom (#68) 2024-05-19 10:34:57 +02:00
Nicolás Hatcher Andrés
f752c90058 UPDATE: Parses selected row/column/range and selected sheet (#67)
* FIX: Update to Rust 1.78.0

* UPDATE: Parses selected row/column/range and selected sheet
2024-05-09 11:46:26 +02:00
Daniel González-Albo
a78d5593f2 Merge pull request #60 from ironcalc/feature/dani-logo
UPDATE: adds missing favicons
2024-04-27 18:11:29 +02:00
Daniel
079208a1bd UPDATE: adds missing favicons 2024-04-27 18:02:04 +02:00
Daniel González-Albo
4721582dfe Merge pull request #42 from ironcalc/feature/dani-logo
UPDATE: adds logo
2024-04-25 19:49:43 +02:00
Daniel
1746eec5da UPDATE: adds logo 2024-04-25 19:42:10 +02:00
Nicolás Hatcher Andrés
f9cf86a17c Bugfix/nicolas more fixes (#36)
* FIX: Remove the serde_json depndendency

* UPDATE: Use binary representation also for languages and locales
2024-04-15 19:25:38 +02:00
Nicolás Hatcher Andrés
49ef846ebd FIX: small diverse fixes (#35) 2024-04-14 21:50:14 +02:00
186 changed files with 8656 additions and 27328 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
target/*
.DS_Store

12
Cargo.lock generated
View File

@@ -370,7 +370,6 @@ dependencies = [
"ryu",
"serde",
"serde_json",
"serde_repr",
]
[[package]]
@@ -679,17 +678,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sha1"
version = "0.10.6"

View File

@@ -1,13 +1,6 @@
all:
cargo build --release
cd bindings/wasm/ && make
cd webapp && npm install && npm run build
lint:
cargo fmt -- --check
# TODO: See issue #33
# cargo clippy --all-targets --all-features -- -D warnings -D clippy::expect_used -D clippy::unwrap_used -D clippy::panic
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --all-targets --all-features
format:
cargo fmt
@@ -17,7 +10,7 @@ tests: lint
./target/debug/documentation
cmp functions.md wiki/functions.md || exit 1
make remove-artifacts
cd bindings/wasm/ && make tests
cd bindings/wasm/ && wasm-pack build --target nodejs && node tests/test.mjs
remove-artifacts:
rm -f xlsx/hello-calc.xlsx
@@ -32,7 +25,6 @@ clean: remove-artifacts
rm -f cargo-test-*
rm -f base/cargo-test-*
rm -f xlsx/cargo-test-*
rm -r -f webapp/node_modules
coverage:

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
assets/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

BIN
assets/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,8 @@
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="600" height="600" rx="20" fill="#F2994A"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 33 KiB

BIN
assets/logo/png/black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
assets/logo/png/white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -12,8 +12,6 @@ readme = "README.md"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"
ryu = "1.0"
chrono = "0.4"
chrono-tz = "0.9"
@@ -21,6 +19,9 @@ regex = "1.0"
once_cell = "1.16.0"
bitcode = "0.6.0"
[dev-dependencies]
serde_json = "1.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = { version = "0.3.69" }

View File

@@ -1,12 +1,9 @@
use crate::{
expressions::token::Error, language::Language, number_format::to_excel_precision_str, types::*,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
/// A CellValue is the representation of the cell content.
#[derive(Serialize, Deserialize, Debug, PartialEq)]
#[serde(untagged)]
#[derive(Debug, PartialEq)]
pub enum CellValue {
None,
String(String),
@@ -14,17 +11,6 @@ pub enum CellValue {
Boolean(bool),
}
impl CellValue {
pub fn to_json_str(&self) -> String {
match &self {
CellValue::None => "null".to_string(),
CellValue::String(s) => json!(s).to_string(),
CellValue::Number(f) => json!(f).to_string(),
CellValue::Boolean(b) => json!(b).to_string(),
}
}
}
impl From<f64> for CellValue {
fn from(value: f64) -> Self {
Self::Number(value)

View File

@@ -222,7 +222,7 @@ impl Parser {
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
self.lexer.set_formula(formula);
self.context = context.clone();
self.context.clone_from(context);
self.parse_expr()
}

View File

@@ -2,7 +2,6 @@ use std::fmt;
use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use crate::language::Language;
@@ -81,8 +80,7 @@ impl fmt::Display for OpProduct {
/// * "#ERROR!" means there was an error processing the formula (for instance "=A1+")
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
/// Note that they are serialized/deserialized by index
#[derive(Serialize_repr, Deserialize_repr, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[repr(u8)]
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub enum Error {
REF,
NAME,

View File

@@ -0,0 +1 @@
PfrendeesD<>VRAITRUEWAHRVERDADEROTVFAUXFALSEFALSCHFALSOUw#REF!#REF!#BEZUG!#¡REF!e<>#NOM?#NAME?#NAME?#¿NOMBRE?x<>#VALEUR!#VALUE!#WERT!#¡VALOR!w<>#DIV/0!#DIV/0!#DIV/0!#¡DIV/0!<04>#N/A#N/A#NV#N/AXv#NOMBRE!#NUM!#ZAHL!#¡NUM!<02><>#N/IMPL!#N/IMPL!#N/IMPL!#N/IMPL!w{#SPILL!#SPILL!#ÜBERLAUF!#SPILL!ff#CALC!#CALC!#CALC!#CALC!ff#CIRC!#CIRC!#CIRC!#CIRC!ww#ERROR!#ERROR!#ERROR!#ERROR!ff#NULL!#NULL!#NULL!#NULL!

View File

@@ -1,15 +1,15 @@
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Clone)]
use bitcode::{Decode, Encode};
use once_cell::sync::Lazy;
#[derive(Encode, Decode, Clone)]
pub struct Booleans {
pub r#true: String,
pub r#false: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct Errors {
pub r#ref: String,
pub name: String,
@@ -25,14 +25,14 @@ pub struct Errors {
pub null: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct Language {
pub booleans: Booleans,
pub errors: Errors,
}
static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
serde_json::from_str(include_str!("language.json")).expect("Failed parsing language file")
bitcode::decode(include_bytes!("language.bin")).expect("Failed parsing language file")
});
pub fn get_language(id: &str) -> Result<&Language, String> {

BIN
base/src/locale/locales.bin Normal file

Binary file not shown.

View File

@@ -1,32 +1,29 @@
use bitcode::{Decode, Encode};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct Locale {
pub dates: Dates,
pub numbers: NumbersProperties,
pub currency: Currency,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct Currency {
pub iso: String,
pub symbol: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct NumbersProperties {
#[serde(rename = "symbols-numberSystem-latn")]
pub symbols: NumbersSymbols,
#[serde(rename = "decimalFormats-numberSystem-latn")]
pub decimal_formats: DecimalFormats,
#[serde(rename = "currencyFormats-numberSystem-latn")]
pub currency_formats: CurrencyFormats,
}
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct Dates {
pub day_names: Vec<String>,
pub day_names_short: Vec<String>,
@@ -35,8 +32,7 @@ pub struct Dates {
pub months_letter: Vec<String>,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
#[derive(Encode, Decode, Clone)]
pub struct NumbersSymbols {
pub decimal: String,
pub group: String,
@@ -54,31 +50,23 @@ pub struct NumbersSymbols {
}
// See: https://cldr.unicode.org/translation/number-currency-formats/number-and-currency-patterns
#[derive(Serialize, Deserialize, Clone)]
#[derive(Encode, Decode, Clone)]
pub struct CurrencyFormats {
pub standard: String,
#[serde(rename = "standard-alphaNextToNumber")]
#[serde(skip_serializing_if = "Option::is_none")]
pub standard_alpha_next_to_number: Option<String>,
#[serde(rename = "standard-noCurrency")]
pub standard_no_currency: String,
pub accounting: String,
#[serde(rename = "accounting-alphaNextToNumber")]
#[serde(skip_serializing_if = "Option::is_none")]
pub accounting_alpha_next_to_number: Option<String>,
#[serde(rename = "accounting-noCurrency")]
pub accounting_no_currency: String,
}
#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
#[derive(Encode, Decode, Clone)]
pub struct DecimalFormats {
pub standard: String,
}
static LOCALES: Lazy<HashMap<String, Locale>> = Lazy::new(|| {
serde_json::from_str(include_str!("locales.json")).expect("Failed parsing locale")
});
static LOCALES: Lazy<HashMap<String, Locale>> =
Lazy::new(|| bitcode::decode(include_bytes!("locales.bin")).expect("Failed parsing locale"));
pub fn get_locale(id: &str) -> Result<&Locale, String> {
// TODO: pass the locale once we implement locales in Rust

View File

@@ -118,6 +118,8 @@ pub struct Model {
pub(crate) language: Language,
/// The timezone used to evaluate the model
pub(crate) tz: Tz,
/// The view id. A view consist of a selected sheet and ranges.
pub(crate) view_id: u32,
}
// FIXME: Maybe this should be the same as CellReference
@@ -886,6 +888,7 @@ impl Model {
language,
locale,
tz,
view_id: 0,
};
model.parse_formulas();

View File

@@ -6,14 +6,18 @@ use crate::{
calc_result::Range,
expressions::{
lexer::LexerMode,
parser::stringify::{rename_sheet_in_node, to_rc_format},
parser::Parser,
parser::{
stringify::{rename_sheet_in_node, to_rc_format},
Parser,
},
types::CellReferenceRC,
},
language::get_language,
locale::get_locale,
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
types::{Metadata, SheetState, Workbook, WorkbookSettings, Worksheet},
types::{
Metadata, SheetState, Workbook, WorkbookSettings, WorkbookView, Worksheet, WorksheetView,
},
utils::ParsedReference,
};
@@ -33,7 +37,20 @@ fn is_valid_sheet_name(name: &str) -> bool {
impl Model {
/// Creates a new worksheet. Note that it does not check if the name or the sheet_id exists
fn new_empty_worksheet(name: &str, sheet_id: u32) -> Worksheet {
fn new_empty_worksheet(name: &str, sheet_id: u32, view_ids: &[&u32]) -> Worksheet {
let mut views = HashMap::new();
for id in view_ids {
views.insert(
**id,
WorksheetView {
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 1,
left_column: 1,
},
);
}
Worksheet {
cols: vec![],
rows: vec![],
@@ -48,6 +65,7 @@ impl Model {
color: Default::default(),
frozen_columns: 0,
frozen_rows: 0,
views,
}
}
@@ -122,7 +140,7 @@ impl Model {
self.parsed_defined_names = parsed_defined_names;
}
// Reparses all formulas and defined names
/// Reparses all formulas and defined names
pub(crate) fn reset_parsed_structures(&mut self) {
self.parser
.set_worksheets(self.workbook.get_worksheet_names());
@@ -153,7 +171,8 @@ impl Model {
let sheet_name = format!("{}{}", base_name, index);
// Now we need a sheet_id
let sheet_id = self.get_new_sheet_id();
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id);
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id, &view_ids);
self.workbook.worksheets.push(worksheet);
self.reset_parsed_structures();
(sheet_name, self.workbook.worksheets.len() as u32 - 1)
@@ -184,7 +203,8 @@ impl Model {
Some(id) => id,
None => self.get_new_sheet_id(),
};
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id);
let view_ids: Vec<&u32> = self.workbook.views.keys().collect();
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id, &view_ids);
if sheet_index as usize > self.workbook.worksheets.len() {
return Err("Sheet index out of range".to_string());
}
@@ -331,11 +351,14 @@ impl Model {
// "2020-08-06T21:20:53Z
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
let mut views = HashMap::new();
views.insert(0, WorkbookView { sheet: 0 });
// String versions of the locale are added here to simplify the serialize/deserialize logic
let workbook = Workbook {
shared_strings: vec![],
defined_names: vec![],
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1)],
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1, &[&0])],
styles: Default::default(),
name: name.to_string(),
settings: WorkbookSettings {
@@ -351,6 +374,7 @@ impl Model {
last_modified: now,
},
tables: HashMap::new(),
views,
};
let parsed_formulas = Vec::new();
let worksheets = &workbook.worksheets;
@@ -371,6 +395,7 @@ impl Model {
locale,
language,
tz,
view_id: 0,
};
model.parse_formulas();
Ok(model)

View File

@@ -76,10 +76,16 @@ fn fn_imconjugate() {
fn fn_imcos() {
let mut model = new_empty_model();
model._set("A1", r#"=IMCOS("4+3i")"#);
// In macos non intel this is "-6.58066304055116+7.58155274274655i"
model._set("A2", r#"=COMPLEX(-6.58066304055116, 7.58155274274654)"#);
model._set("A3", r#"=IMABS(IMSUB(A1, A2)) < G1"#);
// small number
model._set("G1", "0.0000001");
model.evaluate();
assert_eq!(model._get_text("A1"), "-6.58066304055116+7.58155274274654i");
assert_eq!(model._get_text("A3"), "TRUE");
}
#[test]

View File

@@ -53,4 +53,5 @@ mod test_frozen_rows_and_columns;
mod test_get_cell_content;
mod test_percentage;
mod test_today;
mod test_types;
mod user_model;

View File

@@ -0,0 +1,24 @@
#![allow(clippy::unwrap_used)]
use crate::types::{Alignment, HorizontalAlignment, VerticalAlignment};
#[test]
fn alignment_default() {
let alignment = Alignment::default();
assert_eq!(
alignment,
Alignment {
horizontal: HorizontalAlignment::General,
vertical: VerticalAlignment::Bottom,
wrap_text: false
}
);
let s = serde_json::to_string(&alignment).unwrap();
// defaults stringifies as an empty object
assert_eq!(s, "{}");
let a: Alignment = serde_json::from_str("{}").unwrap();
assert_eq!(a, alignment)
}

View File

@@ -8,3 +8,4 @@ mod test_row_column;
mod test_styles;
mod test_to_from_bytes;
mod test_undo_redo;
mod test_view;

View File

@@ -144,13 +144,18 @@ fn basic_fill() {
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.fill.bg_color, None);
assert_eq!(style.fill.fg_color, None);
// bg_color
model
.update_range_style(&range, "fill.bg_color", "#F2F2F2")
.unwrap();
model
.update_range_style(&range, "fill.fg_color", "#F3F4F5")
.unwrap();
let style = model.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
let send_queue = model.flush_send_queue();
@@ -159,6 +164,7 @@ fn basic_fill() {
let style = model2.get_cell_style(0, 1, 1).unwrap();
assert_eq!(style.fill.bg_color, Some("#F2F2F2".to_owned()));
assert_eq!(style.fill.fg_color, Some("#F3F4F5".to_owned()));
}
#[test]
@@ -171,9 +177,15 @@ fn fill_errors() {
width: 1,
height: 1,
};
assert!(model
.update_range_style(&range, "fill.bg_color", "#FFF")
.is_err());
assert_eq!(
model.update_range_style(&range, "fill.bg_color", "#FFF"),
Err("Invalid color: '#FFF'.".to_string())
);
assert_eq!(
model.update_range_style(&range, "fill.fg_color", "#FFF"),
Err("Invalid color: '#FFF'.".to_string())
);
}
#[test]

View File

@@ -0,0 +1,216 @@
#![allow(clippy::unwrap_used)]
use std::collections::HashMap;
use crate::{
constants::{LAST_COLUMN, LAST_ROW},
test::util::new_empty_model,
user_model::SelectedView,
UserModel,
};
#[test]
fn initial_view() {
let model = new_empty_model();
let model = UserModel::from_model(model);
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 1, 1));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 1,
left_column: 1
}
);
}
#[test]
fn set_the_cell_sets_the_range() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_selected_cell(5, 4).unwrap();
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 5, 4));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 5,
column: 4,
range: [5, 4, 5, 4],
top_row: 1,
left_column: 1
}
);
}
#[test]
fn set_the_range_does_not_set_the_cell() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_selected_range(5, 4, 10, 6).unwrap();
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 1, 1));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [5, 4, 10, 6],
top_row: 1,
left_column: 1
}
);
}
#[test]
fn add_new_sheet_and_back() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.new_sheet();
assert_eq!(model.get_selected_sheet(), 0);
model.set_selected_cell(5, 4).unwrap();
model.set_selected_sheet(1).unwrap();
assert_eq!(model.get_selected_cell(), (1, 1, 1));
model.set_selected_sheet(0).unwrap();
assert_eq!(model.get_selected_cell(), (0, 5, 4));
}
#[test]
fn set_selected_cell_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
assert_eq!(
model.set_selected_cell(-5, 4),
Err("Invalid row: '-5'".to_string())
);
assert_eq!(
model.set_selected_cell(5, -4),
Err("Invalid column: '-4'".to_string())
);
assert_eq!(
model.set_selected_range(-1, 1, 1, 1),
Err("Invalid row: '-1'".to_string())
);
assert_eq!(
model.set_selected_range(1, 0, 1, 1),
Err("Invalid column: '0'".to_string())
);
assert_eq!(
model.set_selected_range(1, 1, LAST_ROW + 1, 1),
Err("Invalid row: '1048577'".to_string())
);
assert_eq!(
model.set_selected_range(1, 1, 1, LAST_COLUMN + 1),
Err("Invalid column: '16385'".to_string())
);
}
#[test]
fn set_selected_cell_errors_wrong_sheet() {
let mut model = new_empty_model();
// forcefully set a wrong index
model.workbook.views.get_mut(&0).unwrap().sheet = 2;
let mut model = UserModel::from_model(model);
// It's returning the wrong number
assert_eq!(model.get_selected_sheet(), 2);
// But we can't set the selected cell anymore
assert_eq!(
model.set_selected_cell(3, 4),
Err("Invalid worksheet index 2".to_string())
);
assert_eq!(
model.set_selected_range(3, 4, 5, 6),
Err("Invalid worksheet index 2".to_string())
);
assert_eq!(
model.set_top_left_visible_cell(3, 4),
Err("Invalid worksheet index 2".to_string())
);
// we can fix it by setting the right cell
model.set_selected_sheet(0).unwrap();
model.set_selected_cell(3, 4).unwrap();
}
#[test]
fn set_visible_cell() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
model.set_top_left_visible_cell(100, 12).unwrap();
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 100,
left_column: 12
}
);
let s = serde_json::to_string(&model.get_selected_view()).unwrap();
assert_eq!(
serde_json::from_str::<SelectedView>(&s).unwrap(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 100,
left_column: 12
}
);
}
#[test]
fn set_visible_cell_errors() {
let model = new_empty_model();
let mut model = UserModel::from_model(model);
assert_eq!(
model.set_top_left_visible_cell(-100, 12),
Err("Invalid row: '-100'".to_string())
);
assert_eq!(
model.set_top_left_visible_cell(100, -12),
Err("Invalid column: '-12'".to_string())
);
}
#[test]
fn errors_no_views() {
let mut model = new_empty_model();
// forcefully remove the view
model.workbook.views = HashMap::new();
// also in the sheet
model.workbook.worksheets[0].views = HashMap::new();
let mut model = UserModel::from_model(model);
// get methods will return defaults
assert_eq!(model.get_selected_sheet(), 0);
assert_eq!(model.get_selected_cell(), (0, 1, 1));
assert_eq!(
model.get_selected_view(),
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 1,
left_column: 1
}
);
// set methods won't complain. but won't work either
model.set_selected_sheet(0).unwrap();
model.set_selected_cell(5, 6).unwrap();
assert_eq!(model.get_selected_cell(), (0, 1, 1));
}

View File

@@ -4,37 +4,15 @@ use std::{collections::HashMap, fmt::Display};
use crate::expressions::token::Error;
// Useful for `#[serde(default = "default_as_true")]`
fn default_as_true() -> bool {
true
}
fn default_as_false() -> bool {
false
}
// Useful for `#[serde(skip_serializing_if = "is_true")]`
fn is_true(b: &bool) -> bool {
*b
}
fn is_false(b: &bool) -> bool {
!*b
}
fn is_zero(num: &i32) -> bool {
*num == 0
}
fn is_default_alignment(o: &Option<Alignment>) -> bool {
o.is_none() || *o == Some(Alignment::default())
}
fn hashmap_is_empty(h: &HashMap<String, Table>) -> bool {
h.values().len() == 0
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Metadata {
pub application: String,
pub app_version: String,
@@ -44,14 +22,21 @@ pub struct Metadata {
pub last_modified: String, //"2020-11-20T16:24:35"
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct WorkbookSettings {
pub tz: String,
pub locale: String,
}
/// A Workbook View tracks of the selected sheet for each view
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct WorkbookView {
/// The index of the currently selected sheet.
pub sheet: u32,
}
/// An internal representation of an IronCalc Workbook
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
#[serde(deny_unknown_fields)]
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Workbook {
pub shared_strings: Vec<String>,
pub defined_names: Vec<DefinedName>,
@@ -60,28 +45,22 @@ pub struct Workbook {
pub name: String,
pub settings: WorkbookSettings,
pub metadata: Metadata,
#[serde(default)]
#[serde(skip_serializing_if = "hashmap_is_empty")]
pub tables: HashMap<String, Table>,
pub views: HashMap<u32, WorkbookView>,
}
/// A defined name. The `sheet_id` is the sheet index in case the name is local
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct DefinedName {
pub name: String,
pub formula: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub sheet_id: Option<u32>,
}
// TODO: Move to worksheet.rs make frozen_rows/columns private and u32
/// Internal representation of a worksheet Excel object
/// * state:
/// 18.18.68 ST_SheetState (Sheet Visibility Types)
/// hidden, veryHidden, visible
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[serde(rename_all = "lowercase")]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub enum SheetState {
Visible,
Hidden,
@@ -98,8 +77,25 @@ impl Display for SheetState {
}
}
/// Represents the state of the worksheet as seen by the user. This includes
/// details such as the currently selected cell, the visible range, and the
/// position of the viewport.
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct WorksheetView {
/// The row index of the currently selected cell.
pub row: i32,
/// The column index of the currently selected cell.
pub column: i32,
/// The selected range in the worksheet, specified as [start_row, start_column, end_row, end_column].
pub range: [i32; 4],
/// The row index of the topmost visible cell in the worksheet view.
pub top_row: i32,
/// The column index of the leftmost visible cell in the worksheet view.
pub left_column: i32,
}
/// Internal representation of a worksheet Excel object
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Worksheet {
pub dimension: String,
pub cols: Vec<Col>,
@@ -109,16 +105,12 @@ pub struct Worksheet {
pub shared_formulas: Vec<String>,
pub sheet_id: u32,
pub state: SheetState,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
pub merge_cells: Vec<String>,
pub comments: Vec<Comment>,
#[serde(default)]
#[serde(skip_serializing_if = "is_zero")]
pub frozen_rows: i32,
#[serde(default)]
#[serde(skip_serializing_if = "is_zero")]
pub frozen_columns: i32,
pub views: HashMap<u32, WorksheetView>,
}
/// Internal representation of Excel's sheet_data
@@ -126,7 +118,7 @@ pub struct Worksheet {
pub type SheetData = HashMap<i32, HashMap<i32, Cell>>;
// ECMA-376-1:2016 section 18.3.1.73
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Row {
/// Row index
pub r: i32,
@@ -134,23 +126,19 @@ pub struct Row {
pub custom_format: bool,
pub custom_height: bool,
pub s: i32,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub hidden: bool,
}
// ECMA-376-1:2016 section 18.3.1.13
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Clone)]
pub struct Col {
// Column definitions are defined on ranges, unlike rows which store unique, per-row entries.
/// First column affected by this record. Settings apply to column in \[min, max\] range.
pub min: i32,
/// Last column affected by this record. Settings apply to column in \[min, max\] range.
pub max: i32,
pub width: f64,
pub custom_width: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub style: Option<i32>,
}
@@ -165,32 +153,55 @@ pub enum CellType {
CompoundData = 128,
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, Clone, PartialEq)]
#[serde(tag = "t", deny_unknown_fields)]
#[derive(Encode, Decode, Debug, Clone, PartialEq)]
pub enum Cell {
#[serde(rename = "empty")]
EmptyCell { s: i32 },
#[serde(rename = "b")]
BooleanCell { v: bool, s: i32 },
#[serde(rename = "n")]
NumberCell { v: f64, s: i32 },
EmptyCell {
s: i32,
},
BooleanCell {
v: bool,
s: i32,
},
NumberCell {
v: f64,
s: i32,
},
// Maybe we should not have this type. In Excel this is just a string
#[serde(rename = "e")]
ErrorCell { ei: Error, s: i32 },
ErrorCell {
ei: Error,
s: i32,
},
// Always a shared string
#[serde(rename = "s")]
SharedString { si: i32, s: i32 },
SharedString {
si: i32,
s: i32,
},
// Non evaluated Formula
#[serde(rename = "u")]
CellFormula { f: i32, s: i32 },
#[serde(rename = "fb")]
CellFormulaBoolean { f: i32, v: bool, s: i32 },
#[serde(rename = "fn")]
CellFormulaNumber { f: i32, v: f64, s: i32 },
CellFormula {
f: i32,
s: i32,
},
CellFormulaBoolean {
f: i32,
v: bool,
s: i32,
},
CellFormulaNumber {
f: i32,
v: f64,
s: i32,
},
// always inline string
#[serde(rename = "str")]
CellFormulaString { f: i32, v: String, s: i32 },
#[serde(rename = "fe")]
CellFormulaString {
f: i32,
v: String,
s: i32,
},
CellFormulaError {
f: i32,
ei: Error,
@@ -209,17 +220,16 @@ impl Default for Cell {
}
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Comment {
pub text: String,
pub author_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub author_id: Option<String>,
pub cell_ref: String,
}
// ECMA-376-1:2016 section 18.5.1.2
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Table {
pub name: String,
pub display_name: String,
@@ -227,34 +237,24 @@ pub struct Table {
pub reference: String,
pub totals_row_count: u32,
pub header_row_count: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_dxf_id: Option<u32>,
pub columns: Vec<TableColumn>,
pub style_info: TableStyleInfo,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub has_filters: bool,
}
// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum?
// the totals_row_function is an enum not String methinks
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct TableColumn {
pub id: u32,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub header_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_dxf_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub totals_row_function: Option<String>,
}
@@ -272,25 +272,16 @@ impl Default for TableColumn {
}
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
pub struct TableStyleInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_first_column: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_last_column: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_row_stripes: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub show_column_stripes: bool,
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct Styles {
pub num_fmts: Vec<NumFmt>,
pub fonts: Vec<Font>,
@@ -326,7 +317,7 @@ pub struct Style {
pub quote_prefix: bool,
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct NumFmt {
pub num_fmt_id: i32,
pub format_code: String,
@@ -516,29 +507,17 @@ pub struct Alignment {
pub wrap_text: bool,
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct CellStyleXfs {
pub num_fmt_id: i32,
pub font_id: i32,
pub fill_id: i32,
pub border_id: i32,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_number_format: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_border: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_alignment: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_protection: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_font: bool,
#[serde(default = "default_as_true")]
#[serde(skip_serializing_if = "is_true")]
pub apply_fill: bool,
}
@@ -559,39 +538,24 @@ impl Default for CellStyleXfs {
}
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, Default)]
pub struct CellXfs {
pub xf_id: i32,
pub num_fmt_id: i32,
pub font_id: i32,
pub fill_id: i32,
pub border_id: i32,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_number_format: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_border: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_alignment: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_protection: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_font: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub apply_fill: bool,
#[serde(default = "default_as_false")]
#[serde(skip_serializing_if = "is_false")]
pub quote_prefix: bool,
#[serde(skip_serializing_if = "is_default_alignment")]
pub alignment: Option<Alignment>,
}
#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)]
#[derive(Encode, Decode, Debug, PartialEq, Eq, Clone)]
pub struct CellStyles {
pub name: String,
pub xf_id: i32,

View File

@@ -3,6 +3,7 @@
use std::{collections::HashMap, fmt::Debug};
use bitcode::{Decode, Encode};
use serde::{Deserialize, Serialize};
use crate::{
constants,
@@ -18,6 +19,17 @@ use crate::{
utils::is_valid_hex_color,
};
#[derive(Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Debug))]
pub struct SelectedView {
pub sheet: u32,
pub row: i32,
pub column: i32,
pub range: [i32; 4],
pub top_row: i32,
pub left_column: i32,
}
#[derive(Clone, Encode, Decode)]
struct RowData {
row: Option<Row>,
@@ -118,6 +130,7 @@ enum Diff {
old_value: String,
new_value: String,
},
// FIXME: we are missing SetViewDiffs
}
type DiffList = Vec<Diff>;
@@ -249,7 +262,7 @@ fn vertical(value: &str) -> Result<VerticalAlignment, String> {
}
/// # A wrapper around [`Model`] for a spreadsheet end user.
/// UserModel is a wrapper around Model with undo/redo history, _diffs_ and automatic evaluation.
/// UserModel is a wrapper around Model with undo/redo history, _diffs_, automatic evaluation and view management.
///
/// A diff in this context (or more correctly a _user diff_) is a change created by a user.
///
@@ -849,7 +862,7 @@ impl UserModel {
style.fill.fg_color = color(value)?;
}
"num_fmt" => {
style.num_fmt = value.to_owned();
value.clone_into(&mut style.num_fmt);
}
"border.left" => {
style.border.left = border(value)?;
@@ -935,6 +948,165 @@ impl UserModel {
self.model.get_worksheets_properties()
}
/// Returns the selected sheet index
pub fn get_selected_sheet(&self) -> u32 {
if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
}
}
/// Returns the selected cell
pub fn get_selected_cell(&self) -> (u32, i32, i32) {
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
};
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
if let Some(view) = worksheet.views.get(&self.model.view_id) {
return (sheet, view.row, view.column);
}
}
// return a safe default
(0, 1, 1)
}
/// Returns selected view
pub fn get_selected_view(&self) -> SelectedView {
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
};
if let Ok(worksheet) = self.model.workbook.worksheet(sheet) {
if let Some(view) = worksheet.views.get(&self.model.view_id) {
return SelectedView {
sheet,
row: view.row,
column: view.column,
range: view.range,
top_row: view.top_row,
left_column: view.left_column,
};
}
}
// return a safe default
SelectedView {
sheet: 0,
row: 1,
column: 1,
range: [1, 1, 1, 1],
top_row: 1,
left_column: 1,
}
}
/// Sets the the selected sheet
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), String> {
if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet));
}
if let Some(view) = self.model.workbook.views.get_mut(&0) {
view.sheet = sheet;
}
Ok(())
}
/// Sets the selected cell
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), String> {
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
};
if !is_valid_column_number(column) {
return Err(format!("Invalid column: '{column}'"));
}
if !is_valid_column_number(row) {
return Err(format!("Invalid row: '{row}'"));
}
if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet));
}
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) {
view.row = row;
view.column = column;
view.range = [row, column, row, column];
}
}
Ok(())
}
/// Sets the selected range
pub fn set_selected_range(
&mut self,
start_row: i32,
start_column: i32,
end_row: i32,
end_column: i32,
) -> Result<(), String> {
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
};
if !is_valid_column_number(start_column) {
return Err(format!("Invalid column: '{start_column}'"));
}
if !is_valid_column_number(start_row) {
return Err(format!("Invalid row: '{start_row}'"));
}
if !is_valid_column_number(end_column) {
return Err(format!("Invalid column: '{end_column}'"));
}
if !is_valid_column_number(end_row) {
return Err(format!("Invalid row: '{end_row}'"));
}
if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet));
}
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) {
view.range = [start_row, start_column, end_row, end_column];
}
}
Ok(())
}
/// Sets the value of the first visible cell
pub fn set_top_left_visible_cell(
&mut self,
top_row: i32,
left_column: i32,
) -> Result<(), String> {
let sheet = if let Some(view) = self.model.workbook.views.get(&self.model.view_id) {
view.sheet
} else {
0
};
if !is_valid_column_number(left_column) {
return Err(format!("Invalid column: '{left_column}'"));
}
if !is_valid_column_number(top_row) {
return Err(format!("Invalid row: '{top_row}'"));
}
if self.model.workbook.worksheet(sheet).is_err() {
return Err(format!("Invalid worksheet index {}", sheet));
}
if let Ok(worksheet) = self.model.workbook.worksheet_mut(sheet) {
if let Some(view) = worksheet.views.get_mut(&0) {
view.top_row = top_row;
view.left_column = left_column;
}
}
Ok(())
}
// **** Private methods ****** //
fn push_diff_list(&mut self, diff_list: DiffList) {

View File

@@ -286,4 +286,56 @@ impl Model {
pub fn get_worksheets_properties(&self) -> JsValue {
serde_wasm_bindgen::to_value(&self.model.get_worksheets_properties()).unwrap()
}
#[wasm_bindgen(js_name = "getSelectedSheet")]
pub fn get_selected_sheet(&self) -> u32 {
self.model.get_selected_sheet()
}
#[wasm_bindgen(js_name = "getSelectedCell")]
pub fn get_selected_cell(&self) -> Vec<i32> {
let (sheet, row, column) = self.model.get_selected_cell();
vec![sheet as i32, row, column]
}
#[wasm_bindgen(js_name = "getSelectedView")]
pub fn get_selected_view(&self) -> JsValue {
serde_wasm_bindgen::to_value(&self.model.get_selected_view()).unwrap()
}
#[wasm_bindgen(js_name = "setSelectedSheet")]
pub fn set_selected_sheet(&mut self, sheet: u32) -> Result<(), JsError> {
self.model.set_selected_sheet(sheet).map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setSelectedCell")]
pub fn set_selected_cell(&mut self, row: i32, column: i32) -> Result<(), JsError> {
self.model
.set_selected_cell(row, column)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setSelectedRange")]
pub fn set_selected_range(
&mut self,
start_row: i32,
start_column: i32,
end_row: i32,
end_column: i32,
) -> Result<(), JsError> {
self.model
.set_selected_range(start_row, start_column, end_row, end_column)
.map_err(to_js_error)
}
#[wasm_bindgen(js_name = "setTopLeftVisibleCell")]
pub fn set_top_left_visible_cell(
&mut self,
top_row: i32,
top_column: i32,
) -> Result<(), JsError> {
self.model
.set_top_left_visible_cell(top_row, top_column)
.map_err(to_js_error)
}
}

View File

@@ -1,3 +1,2 @@
node_modules/*
dist/*
example.json

View File

@@ -0,0 +1,19 @@
import type { StorybookConfig } from "storybook-solidjs-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@chromatic-com/storybook",
"@storybook/addon-interactions",
],
framework: {
name: "storybook-solidjs-vite",
options: {},
},
docs: {
autodocs: "tag",
},
};
export default config;

View File

@@ -0,0 +1,12 @@
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;

8
solidjs_app/Makefile Normal file
View File

@@ -0,0 +1,8 @@
lint:
pnpm biome lint *
format:
pnpm biome format *
build:
pnpm run build

70
solidjs_app/README.md Normal file
View File

@@ -0,0 +1,70 @@
# Web IronCalc
## Widgets
Toolbar
NavigationBar
FormulaBar
ColorPicker
Number Formatter
Border Picker
## Stack
Vite
TypeScript
SolidJs
Lucide Icons
BiomeJs
Storybook
pnpm
## Recreate
Install nodejs
Activate pnpm
corepack enable pnpm
Create app
pnpm create vite
pnpm install
add biomejs
pnpm add --save-dev --save-exact @biomejs/biome
pnpm biome init
add solidjs
add storybook
pnpm dlx storybook@latest init
add i18n
pnpm add @solid-primitives/i18n
(https://github.com/jfgodoy/vite-plugin-solid-svg)
add vite-plugin-solid-svg
add script: "restore": "cp node_modules/@ironcalc/wasm/wasm_bg.wasm node_modules/.vite/deps/",
## Usage
```bash
$ pnpm install # or npm install or yarn install
```
## Available Scripts
In the project directory, you can run:
### `pnpm run dev`
Runs the app in the development mode.<br>
Open [http://localhost:5173](http://localhost:5173) to view it in the browser.
### `pnpm run build`
Builds the app for production to the `dist` folder.<br>
It correctly bundles Solid in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
## Deployment
Learn more about deploying your application with the [documentations](https://vitejs.dev/guide/static-deploy.html)

15
solidjs_app/biome.json Normal file
View File

@@ -0,0 +1,15 @@
{
"$schema": "https://biomejs.dev/schemas/1.7.0/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"indentStyle": "space"
}
}

13
solidjs_app/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/ironcalc_icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Solid + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

35
solidjs_app/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"restore": "cp node_modules/@ironcalc/wasm/wasm_bg.wasm node_modules/.vite/deps/",
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@ironcalc/wasm": "file:../bindings/wasm/pkg",
"@solid-primitives/i18n": "^2.1.1",
"lucide-solid": "^0.379.0",
"solid-js": "^1.8.15"
},
"devDependencies": {
"@biomejs/biome": "1.7.0",
"@chromatic-com/storybook": "^1.3.3",
"@storybook/addon-essentials": "^8.0.8",
"@storybook/addon-interactions": "^8.0.8",
"@storybook/addon-links": "^8.0.8",
"@storybook/blocks": "^8.0.8",
"storybook": "^8.0.8",
"storybook-solidjs": "^1.0.0-beta.2",
"storybook-solidjs-vite": "^1.0.0-beta.2",
"typescript": "^5.2.2",
"vite": "^5.2.0",
"vite-plugin-solid": "^2.10.2",
"vite-plugin-solid-svg": "^0.8.1"
}
}

6864
solidjs_app/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="600" height="600" rx="20" fill="#F2994A"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

7
solidjs_app/src/App.css Normal file
View File

@@ -0,0 +1,7 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}

29
solidjs_app/src/App.tsx Normal file
View File

@@ -0,0 +1,29 @@
import { Show, createResource } from "solid-js";
// import "./App.css";
// import solidLogo from "./assets/solid.svg";
import init, { Model } from "@ironcalc/wasm";
import Workbook from "./components/Workbook";
const fetchModel = async () => {
await init();
// const model_bytes = new Uint8Array(
// await (await fetch("./example.ic")).arrayBuffer(),
// );
// const _model = Model.from_bytes(model_bytes);*/
const model = new Model("en", "UTC");
model.setUserInput(0, 1, 1, "=1+1");
return model;
};
function App() {
const [model] = createResource(fetchModel);
return (
<Show when={model()} fallback={<div>Loading...</div>}>
{(model) => <Workbook model={model()} />}
</Show>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,8 @@
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="600" height="600" rx="20" fill="#F2994A"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 100C348.98 166.034 322.748 229.362 276.055 276.055C268.163 283.947 259.796 291.255 251.021 297.95L251.021 500L348.98 500H251.021C251.021 433.966 277.252 370.637 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05L348.98 100Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M251.021 100.068C251.003 140.096 235.094 178.481 206.788 206.787C178.466 235.109 140.053 251.02 100 251.02V348.979C154.873 348.979 207.877 330.866 251.021 297.95V100.068Z" fill="white"/>
<path opacity="0.8" fill-rule="evenodd" clip-rule="evenodd" d="M348.98 499.882C349.011 459.872 364.918 421.507 393.213 393.213C421.534 364.891 459.947 348.98 500 348.98V251.02C445.128 251.02 392.123 269.134 348.98 302.05V499.882Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M276.055 276.055C322.748 229.362 348.98 166.034 348.98 100H251.021V297.95C259.796 291.255 268.163 283.947 276.055 276.055Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M348.98 302.05V499.895C348.98 499.93 348.98 499.965 348.98 500L251.021 500C251.021 499.946 251.02 499.891 251.021 499.837C251.064 433.862 277.291 370.599 323.945 323.945C331.837 316.053 340.204 308.745 348.98 302.05Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,23 @@
import type { Model } from "@ironcalc/wasm";
import styles from "./workbook.module.css";
import Toolbar from "./toolbar/Toolbar";
import Navigation from "./navigation/Navigation";
import FormulaBar from "./formulabar/FormulaBar";
import Worksheet from "./Worksheet/Worksheet";
function Workbook(props: { model: Model }) {
const onkeydown = (event: KeyboardEvent) => {
console.log("key pressed: ", event);
};
return (
<div class={styles.workbook} onkeydown={onkeydown} tabIndex={0}>
<Toolbar></Toolbar>
{/* {props.model.getFormattedCellValue(0, 1, 1)} */}
<FormulaBar></FormulaBar>
<Worksheet></Worksheet>
<Navigation></Navigation>
</div>
);
}
export default Workbook;

View File

@@ -0,0 +1,14 @@
import styles from "./worksheet.module.css";
function Worksheet() {
const onkeydown = (event: KeyboardEvent) => {
console.log("key pressed: ", event);
};
return (
<div class={styles.worksheet} onkeydown={onkeydown} tabIndex={0}>
</div>
);
}
export default Worksheet;

View File

@@ -0,0 +1,14 @@
import styles from "./formulabar.module.css";
function FormulaBar() {
const onkeydown = (event: KeyboardEvent) => {
console.log("key pressed: ", event);
};
return (
<div class={styles.toolbar} onkeydown={onkeydown} tabIndex={0}>
</div>
);
}
export default FormulaBar;

View File

@@ -0,0 +1,14 @@
import styles from "./navigation.module.css";
function Navigation() {
const onkeydown = (event: KeyboardEvent) => {
console.log("key pressed: ", event);
};
return (
<div class={styles.navigation} onkeydown={onkeydown} tabIndex={0}>
</div>
);
}
export default Navigation;

View File

@@ -0,0 +1,299 @@
import { JSX } from "solid-js/jsx-runtime";
import styles from "./toolbar.module.css";
import {
AlignCenter,
AlignLeft,
AlignRight,
ArrowDownToLine,
ArrowUpToLine,
Bold,
ChevronDown,
Euro,
Grid2X2,
Italic,
Paintbrush2,
PaintBucket,
Percent,
Redo2,
Strikethrough,
Type,
Underline,
Undo2,
} from "lucide-solid";
import { DecimalPlacesDecreaseIcon, DecimalPlacesIncreaseIcon, ArrowMiddleFromLine } from "../../icons";
function Toolbar() {
const onkeydown = (event: KeyboardEvent) => {
console.log("key pressed: ", event);
};
const t = (s: string): string => s;
const properties = {
onUndo: () => {},
canUndo: true,
onRedo: () => {},
canRedo: true,
onCopyStyles: () => {},
canEdit: true,
};
return (
<div class={styles.toolbar} onkeydown={onkeydown} tabIndex={0}>
<StyledButton
pressed={false}
onClick={properties.onUndo}
disabled={!properties.canUndo}
title={t("toolbar.undo")}
>
<Undo2 />
</StyledButton>
<StyledButton
pressed={false}
onClick={properties.onRedo}
disabled={!properties.canRedo}
title={t("toolbar.redo")}
>
<Redo2 />
</StyledButton>
<div class={styles.divider} />
<StyledButton
pressed={false}
onClick={properties.onCopyStyles}
title={t("toolbar.copy_styles")}
>
<Paintbrush2 />
</StyledButton>
<div class={styles.divider} />
<StyledButton
pressed={false}
// onClick={(): void => {
// properties.onNumberFormatPicked(NumberFormats.CURRENCY_EUR);
// }}
title={t("toolbar.euro")}
>
<Euro />
</StyledButton>
<StyledButton
pressed={false}
// onClick={(): void => {
// properties.onNumberFormatPicked(NumberFormats.PERCENTAGE);
// }}
title={t("toolbar.percentage")}
>
<Percent />
</StyledButton>
<StyledButton
pressed={false}
// onClick={(): void => {
// properties.onNumberFormatPicked(
// decreaseDecimalPlaces(properties.numFmt)
// );
// }}
title={t("toolbar.decimal_places_decrease")}
>
<div><DecimalPlacesDecreaseIcon /></div>
</StyledButton>
<StyledButton
pressed={false}
// onClick={(): void => {
// properties.onNumberFormatPicked(
// increaseDecimalPlaces(properties.numFmt)
// );
// }}
title={t("toolbar.decimal_places_increase")}
>
<DecimalPlacesIncreaseIcon />
</StyledButton>
{/* // <FormatMenu
// numFmt={properties.numFmt}
// onChange={(numberFmt): void => {
// properties.onNumberFormatPicked(numberFmt);
// }}
// onExited={(): void => {}}
// anchorOrigin={{
// horizontal: 20, // Aligning the menu to the middle of FormatButton
// vertical: "bottom",
// }}
// >*/
<StyledButton
pressed={false}
title={t("toolbar.format_number")}
>
<div class={styles.format_menu}>{"123"}<ChevronDown /></div>
</StyledButton>
/* </FormatMenu> */}
<div class={styles.divider} />
<StyledButton
// pressed={properties.bold}
// onClick={() => properties.onToggleBold(!properties.bold)}
title={t("toolbar.bold")}
>
<Bold />
</StyledButton>
<StyledButton
// pressed={properties.italic}
// onClick={() => properties.onToggleItalic(!properties.italic)}
title={t("toolbar.italic")}
>
<Italic />
</StyledButton>
<StyledButton
// pressed={properties.underline}
// onClick={() => properties.onToggleUnderline(!properties.underline)}
title={t("toolbar.underline")}
>
<Underline />
</StyledButton>
<StyledButton
// pressed={properties.strike}
// onClick={() => properties.onToggleStrike(!properties.strike)}
title={t("toolbar.strike_trough")}
>
<Strikethrough />
</StyledButton>
<div class={styles.divider} />
<StyledButton
pressed={false}
title={t("toolbar.font_color")}
// ref={fontColorButton}
// underlinedColor={properties.fontColor}
// onClick={() => setFontColorPickerOpen(true)}
>
<Type />
</StyledButton>
<StyledButton
pressed={false}
title={t("toolbar.fill_color")}
// ref={fillColorButton}
// underlinedColor={properties.fillColor}
// onClick={() => setFillColorPickerOpen(true)}
>
<PaintBucket />
</StyledButton>
<div class={styles.divider} />
<StyledButton
// pressed={properties.horizontalAlign === "left"}
// onClick={() =>
// properties.onToggleHorizontalAlign(
// properties.horizontalAlign === "left" ? "general" : "left"
// )
// }
title={t("toolbar.align_left")}
>
<AlignLeft />
</StyledButton>
<StyledButton
// pressed={properties.horizontalAlign === "center"}
// onClick={() =>
// properties.onToggleHorizontalAlign(
// properties.horizontalAlign === "center" ? "general" : "center"
// )
// }
title={t("toolbar.align_center")}
>
<AlignCenter />
</StyledButton>
<StyledButton
// pressed={properties.horizontalAlign === "right"}
// onClick={() =>
// properties.onToggleHorizontalAlign(
// properties.horizontalAlign === "right" ? "general" : "right"
// )
// }
title={t("toolbar.align_right")}
>
<AlignRight />
</StyledButton>
<StyledButton
// pressed={properties.verticalAlign === "top"}
// onClick={() =>
// properties.onToggleVerticalAlign(
// properties.verticalAlign === "top" ? "bottom" : "top"
// )
// }
title={t("toolbar.vertical_align_top")}
>
<ArrowUpToLine />
</StyledButton>
<StyledButton
// pressed={properties.verticalAlign === "center"}
// onClick={() =>
// properties.onToggleVerticalAlign(
// properties.verticalAlign === "center" ? "bottom" : "center"
// )
// }
title={t("toolbar.vertical_align_center")}
>
<ArrowMiddleFromLine />
</StyledButton>
<StyledButton
// pressed={properties.verticalAlign === "bottom"}
// onClick={() => properties.onToggleVerticalAlign("bottom")}
title={t("toolbar.vertical_align_bottom")}
>
<ArrowDownToLine />
</StyledButton>
<div class={styles.divider} />
<StyledButton
pressed={false}
// onClick={() => setBorderPickerOpen(true)}
// ref={borderButton}
title={t("toolbar.borders")}
>
<Grid2X2 />
</StyledButton>
{/* // <ColorPicker
// color={properties.fontColor}
// onChange={(color): void => {
// properties.onTextColorPicked(color);
// setFontColorPickerOpen(false);
// }}
// anchorEl={fontColorButton}
// open={fontColorPickerOpen}
// />
// <ColorPicker
// color={properties.fillColor}
// onChange={(color): void => {
// properties.onFillColorPicked(color);
// setFillColorPickerOpen(false);
// }}
// anchorEl={fillColorButton}
// open={fillColorPickerOpen}
// />
// <BorderPicker
// onChange={(border): void => {
// properties.onBorderChanged(border);
// setBorderPickerOpen(false);
// }}
// anchorEl={borderButton}
// open={borderPickerOpen}
// /> */}
</div>
);
}
function StyledButton(props: {
children: JSX.Element;
title: string;
onClick?: () => void;
disabled?: boolean;
pressed?: boolean;
underlinedColor?: string;
}) {
return (
<button
disabled={props.disabled || false}
onClick={props.onClick}
title={props.title}
class={styles.button}
>
{props.children}
</button>
);
}
export default Toolbar;

View File

@@ -0,0 +1,67 @@
.toolbar {
display: flex;
flex-shrink: 0;
align-items: center;
/* ${({ theme }) => theme.palette.background.paper}; */
background: #fff;
height: 40px;
line-height: 40px;
/* theme.palette.grey["600"] */
border-bottom: 1px solid #757575;
font-family: Inter;
border-radius: 4px 4px 0px 0px;
overflow-x: auto;
}
.button {
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 26px;
border: 0px solid #fff;
border-radius: 2px;
margin-right: 5px;
transition: all 0.2s;
cursor: pointer;
background-color: white;
padding: 0px;
}
.button:disabled {
color: grey;
cursor: default;
}
.button:not(disabled) {
border-top: none;
border-bottom: none;
color: #21243a;
background-color: #fff;
}
.button:hover {
background-color: #f1f2f8;
border-top-color: #f1f2f8;
}
.button svg {
width: 16px;
height: 16px;
}
.divider {
width: 0px;
height: 10px;
border-left: 1px solid #d3d6e9;
margin-left: 5px;
margin-right: 10px;
}
.format_menu {
width: 40px;
font-size: 13px;
font-weight: 400;
display: flex;
}

View File

@@ -0,0 +1,6 @@
/* @import './theme.css'; */
.workbook {
/* background-color: var(--main-bg-color); */
/* color: var(--other); */
}

View File

Before

Width:  |  Height:  |  Size: 869 B

After

Width:  |  Height:  |  Size: 869 B

View File

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 538 B

View File

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 498 B

View File

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 498 B

View File

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 586 B

View File

Before

Width:  |  Height:  |  Size: 539 B

After

Width:  |  Height:  |  Size: 539 B

View File

Before

Width:  |  Height:  |  Size: 513 B

After

Width:  |  Height:  |  Size: 513 B

View File

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 542 B

View File

Before

Width:  |  Height:  |  Size: 538 B

After

Width:  |  Height:  |  Size: 538 B

View File

Before

Width:  |  Height:  |  Size: 744 B

After

Width:  |  Height:  |  Size: 744 B

View File

Before

Width:  |  Height:  |  Size: 539 B

After

Width:  |  Height:  |  Size: 539 B

View File

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 659 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1004 B

After

Width:  |  Height:  |  Size: 1004 B

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 929 B

After

Width:  |  Height:  |  Size: 929 B

View File

@@ -0,0 +1,46 @@
import DecimalPlacesDecreaseIcon from "./decrease-decimal.svg";
import DecimalPlacesIncreaseIcon from "./increase-decimal.svg";
import BorderBottomIcon from "./border-bottom.svg";
import BorderCenterHIcon from "./border-center-h.svg";
import BorderCenterVIcon from "./border-center-v.svg";
import BorderInnerIcon from "./border-inner.svg";
import BorderLeftIcon from "./border-left.svg";
import BorderOuterIcon from "./border-outer.svg";
import BorderRightIcon from "./border-right.svg";
import BorderTopIcon from "./border-top.svg";
import BorderNoneIcon from "./border-none.svg";
import BorderStyleIcon from "./border-style.svg";
import DeleteColumnIcon from "./delete-column.svg";
import DeleteRowIcon from "./delete-row.svg";
import InsertColumnLeftIcon from "./insert-column-left.svg";
import InsertColumnRightIcon from "./insert-column-right.svg";
import InsertRowAboveIcon from "./insert-row-above.svg";
import InsertRowBelow from "./insert-row-below.svg";
import ArrowMiddleFromLine from "./arrow-middle-from-line.svg";
import Fx from "./fx.svg";
export {
ArrowMiddleFromLine,
DecimalPlacesDecreaseIcon,
DecimalPlacesIncreaseIcon,
BorderBottomIcon,
BorderCenterHIcon,
BorderCenterVIcon,
BorderInnerIcon,
BorderLeftIcon,
BorderOuterIcon,
BorderRightIcon,
BorderTopIcon,
BorderNoneIcon,
BorderStyleIcon,
DeleteColumnIcon,
DeleteRowIcon,
InsertColumnLeftIcon,
InsertColumnRightIcon,
InsertRowAboveIcon,
InsertRowBelow,
Fx,
};

View File

Before

Width:  |  Height:  |  Size: 726 B

After

Width:  |  Height:  |  Size: 726 B

View File

Before

Width:  |  Height:  |  Size: 725 B

After

Width:  |  Height:  |  Size: 725 B

View File

Before

Width:  |  Height:  |  Size: 706 B

After

Width:  |  Height:  |  Size: 706 B

View File

Before

Width:  |  Height:  |  Size: 706 B

After

Width:  |  Height:  |  Size: 706 B

68
solidjs_app/src/index.css Normal file
View File

@@ -0,0 +1,68 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

12
solidjs_app/src/index.tsx Normal file
View File

@@ -0,0 +1,12 @@
/* @refresh reload */
import { render } from "solid-js/web";
import App from "./App";
// import "./index.css";
import "./theme.css";
const root = document.getElementById("root");
if (root) {
render(() => <App />, root);
}

View File

@@ -0,0 +1,4 @@
:root {
--main-bg-color: brown;
--other: blue;
}

1
solidjs_app/src/typings.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
declare module "*.module.css";

1
solidjs_app/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -2,8 +2,8 @@
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
@@ -12,14 +12,18 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"jsx": "preserve",
"jsxImportSource": "solid-js",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true
"types": [
"vite-plugin-solid-svg/types-component-solid",
"vite/client"
]
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]

View File

@@ -4,7 +4,8 @@
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,8 @@
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
import solidPlugin from "vite-plugin-solid";
import solidSvg from "vite-plugin-solid-svg";
export default defineConfig({
plugins: [solid(), solidPlugin(), solidSvg()],
});

View File

@@ -1,19 +0,0 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;

Some files were not shown because too many files have changed in this diff Show More