diff --git a/Cargo.lock b/Cargo.lock index 7c3d387..300fd46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1228,11 +1228,11 @@ dependencies = [ [[package]] name = "headers" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bytes", "headers-core", "http 1.3.1", @@ -1676,6 +1676,7 @@ dependencies = [ "derive_builder", "dotenvy", "futures", + "headers", "interim-models", "interim-pgtypes", "oauth2", diff --git a/dev-services/docker-compose.yaml b/dev-services/docker-compose.yaml new file mode 100644 index 0000000..1bc9e11 --- /dev/null +++ b/dev-services/docker-compose.yaml @@ -0,0 +1,34 @@ +name: interim + +services: + pg: + image: postgres:17 + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: guest + ports: + - "127.0.0.1:5432:5432" + volumes: + - "./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:ro" + - "./pgdata:/var/lib/postgresql/data" + + keycloak: + depends_on: [pg] + restart: always + build: + context: . + dockerfile: keycloak.dockerfile + environment: + KC_DB: postgres + KC_DB_URL: jdbc:postgresql://pg:5432/keycloak + KC_DB_USERNAME: keycloak + KC_DB_PASSWORD: guest + KC_HOSTNAME: 0.0.0.0 + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: guest + command: [start, --optimized] + ports: + - "127.0.0.1:9000:9000" + - "127.0.0.1:8443:8443" + diff --git a/dev-services/docker-entrypoint-initdb.d/init-app.sql b/dev-services/docker-entrypoint-initdb.d/init-app.sql new file mode 100644 index 0000000..d557f83 --- /dev/null +++ b/dev-services/docker-entrypoint-initdb.d/init-app.sql @@ -0,0 +1,3 @@ +CREATE USER interim_app WITH ENCRYPTED PASSWORD 'guest'; +CREATE DATABASE interim_app; +ALTER DATABASE interim_app OWNER TO interim_app; diff --git a/dev-services/docker-entrypoint-initdb.d/init-keycloak.sql b/dev-services/docker-entrypoint-initdb.d/init-keycloak.sql new file mode 100644 index 0000000..7ff0b08 --- /dev/null +++ b/dev-services/docker-entrypoint-initdb.d/init-keycloak.sql @@ -0,0 +1,3 @@ +CREATE USER keycloak WITH ENCRYPTED PASSWORD 'guest'; +CREATE DATABASE keycloak; +ALTER DATABASE keycloak OWNER TO keycloak; diff --git a/dev-services/keycloak.dockerfile b/dev-services/keycloak.dockerfile new file mode 100644 index 0000000..cb38874 --- /dev/null +++ b/dev-services/keycloak.dockerfile @@ -0,0 +1,18 @@ +FROM quay.io/keycloak/keycloak:26.3.2 as builder + +# Enable health and metrics support +ENV KC_HEALTH_ENABLED=true +ENV KC_METRICS_ENABLED=true + +# Configure a database vendor +ENV KC_DB=postgres + +WORKDIR /opt/keycloak +# for demonstration purposes only, please make sure to use proper certificates in production instead +RUN keytool -genkeypair -storepass password -storetype PKCS12 -keyalg RSA -keysize 2048 -dname "CN=server" -alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore conf/server.keystore +RUN /opt/keycloak/bin/kc.sh build + +FROM quay.io/keycloak/keycloak:26.3.2 +COPY --from=builder /opt/keycloak/ /opt/keycloak/ + +ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] diff --git a/interim-models/src/field.rs b/interim-models/src/field.rs index 3bb0ef8..f34732f 100644 --- a/interim-models/src/field.rs +++ b/interim-models/src/field.rs @@ -36,17 +36,21 @@ impl Field { pub fn webc_tag(&self) -> &str { match self.field_type.0 { - FieldType::Integer => "cell-integer", - FieldType::InterimUser => "cell-interim-user", - FieldType::Text => "cell-text", + FieldType::InterimUser {} => "cell-interim-user", + FieldType::Text {} => "cell-text", FieldType::Timestamp { .. } => "cell-timestamp", - FieldType::Uuid => "cell-uuid", + FieldType::Uuid { .. } => "cell-uuid", FieldType::Unknown => "cell-unknown", } } pub fn webc_custom_attrs(&self) -> Vec<(String, String)> { - vec![] + match self.field_type.clone() { + sqlx::types::Json(FieldType::Uuid { + default_with_version: Some(_), + }) => vec![("has_default".to_owned(), "true".to_owned())], + _ => vec![], + } } pub fn get_value_encodable(&self, row: &PgRow) -> Result { @@ -56,9 +60,6 @@ impl Field { let type_info = value_ref.type_info(); let ty = type_info.name(); Ok(match ty { - "INT" | "INT4" => { - Encodable::Integer( as Decode>::decode(value_ref).unwrap()) - } "TEXT" | "VARCHAR" => { Encodable::Text( as Decode>::decode(value_ref).unwrap()) } @@ -103,13 +104,14 @@ where lens_id = $1 #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "t", content = "c")] pub enum FieldType { - Integer, - InterimUser, - Text, + InterimUser {}, + Text {}, Timestamp { format: String, }, - Uuid, + Uuid { + default_with_version: Option, + }, /// A special variant for when the field type is not specified and cannot be /// inferred. This isn't represented as an error, because we still want to /// be able to define display behavior via the .render() method. @@ -119,12 +121,13 @@ pub enum FieldType { impl FieldType { pub fn default_from_attr(attr: &PgAttribute) -> Self { match attr.regtype.as_str() { - "integer" => Self::Integer, - "text" => Self::Text, + "text" => Self::Text {}, "timestamp" => Self::Timestamp { format: RFC_3339_S.to_owned(), }, - "uuid" => Self::Uuid, + "uuid" => Self::Uuid { + default_with_version: None, + }, _ => Self::Unknown, } } @@ -134,15 +137,28 @@ impl FieldType { /// None if the field type is Unknown. pub fn attr_data_type_fragment(&self) -> Option<&'static str> { match self { - Self::Integer => Some("integer"), - Self::InterimUser | Self::Text => Some("text"), + Self::InterimUser {} | Self::Text {} => Some("text"), Self::Timestamp { .. } => Some("timestamptz"), - Self::Uuid => Some("uuid"), + Self::Uuid { .. } => Some("uuid"), Self::Unknown => None, } } + + pub fn default_for_insert(&self) -> Result { + match self { + Self::InterimUser {} => Ok(Encodable::Text(None)), + Self::Text {} => Ok(Encodable::Text(None)), + Self::Timestamp { .. } => Ok(Encodable::Timestamp(None)), + Self::Uuid { .. } => Ok(Encodable::Uuid(None)), + Self::Unknown => Err(FieldTypeUnknownError {}), + } + } } +#[derive(Clone, Debug, Error)] +#[error("field type is unknown")] +pub struct FieldTypeUnknownError {} + // -------- Insertable -------- #[derive(Builder, Clone, Debug)] @@ -212,9 +228,8 @@ pub enum ParseError { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(tag = "t", content = "c")] pub enum Encodable { - Integer(Option), Text(Option), - Timestamptz(Option>), + Timestamp(Option>), Uuid(Option), } @@ -224,9 +239,8 @@ impl Encodable { query: sqlx::query::Query<'a, Postgres, ::Arguments<'a>>, ) -> sqlx::query::Query<'a, Postgres, ::Arguments<'a>> { match self { - Self::Integer(value) => query.bind(value), Self::Text(value) => query.bind(value), - Self::Timestamptz(value) => query.bind(value), + Self::Timestamp(value) => query.bind(value), Self::Uuid(value) => query.bind(value), } } diff --git a/interim-server/Cargo.toml b/interim-server/Cargo.toml index 7ac2741..82a047c 100644 --- a/interim-server/Cargo.toml +++ b/interim-server/Cargo.toml @@ -15,6 +15,7 @@ config = "0.14.1" derive_builder = { workspace = true } dotenvy = "0.15.7" futures = { workspace = true } +headers = "0.4.1" interim-models = { workspace = true } interim-pgtypes = { workspace = true } oauth2 = "4.4.2" diff --git a/interim-server/src/auth.rs b/interim-server/src/auth.rs index 7e1d2fd..067a8ce 100644 --- a/interim-server/src/auth.rs +++ b/interim-server/src/auth.rs @@ -1,15 +1,15 @@ use anyhow::{Context, Result}; use async_session::{Session, SessionStore}; use axum::{ + Router, extract::{Query, State}, response::{IntoResponse, Redirect}, routing::get, - Router, }; use axum_extra::extract::cookie::{Cookie, CookieJar, SameSite}; use oauth2::{ - basic::BasicClient, reqwest::async_http_client, AuthUrl, AuthorizationCode, ClientId, - ClientSecret, CsrfToken, RedirectUrl, RefreshToken, TokenResponse, TokenUrl, + AuthUrl, AuthorizationCode, ClientId, ClientSecret, CsrfToken, RedirectUrl, RefreshToken, + TokenResponse, TokenUrl, basic::BasicClient, reqwest::async_http_client, }; use serde::{Deserialize, Serialize}; @@ -74,7 +74,7 @@ async fn start_login( if session.get::(SESSION_KEY_AUTH_INFO).is_some() { tracing::debug!("already logged in, redirecting..."); - return Ok(Redirect::to(&format!("{}/", root_path)).into_response()); + return Ok(Redirect::to(&format!("{root_path}/")).into_response()); } assert!(session.get_raw(SESSION_KEY_AUTH_REFRESH_TOKEN).is_none()); diff --git a/interim-server/src/router.rs b/interim-server/src/router.rs index 9d3f45c..6286a40 100644 --- a/interim-server/src/router.rs +++ b/interim-server/src/router.rs @@ -65,6 +65,10 @@ pub fn new_router(state: AppState) -> Router<()> { "/d/{base_id}/r/{class_oid}/l/{lens_id}/", get(routes::lenses::lens_page), ) + .route( + "/d/{base_id}/r/{class_oid}/l/{lens_id}/get-data", + get(routes::lenses::get_data_page_get), + ) // .route( // "/d/{base_id}/r/{class_oid}/l/{lens_id}/add-selection", // post(routes::lenses::add_selection_page_post), diff --git a/interim-server/src/routes/lenses.rs b/interim-server/src/routes/lenses.rs index 294dd87..14045b0 100644 --- a/interim-server/src/routes/lenses.rs +++ b/interim-server/src/routes/lenses.rs @@ -13,7 +13,7 @@ use interim_models::{ lens::{Lens, LensDisplayType}, }; use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::{ postgres::{PgRow, types::Oid}, @@ -205,7 +205,7 @@ pub async fn lens_page( .collect(); #[derive(Template)] - #[template(path = "lens.html")] + #[template(path = "lens0_2.html")] struct ResponseTemplate { fields: Vec, all_columns: Vec, @@ -237,6 +237,94 @@ pub async fn lens_page( .into_response()) } +pub async fn get_data_page_get( + State(settings): State, + State(mut base_pooler): State, + AppDbConn(mut app_db): AppDbConn, + CurrentUser(current_user): CurrentUser, + Path(LensPagePath { + lens_id, + base_id, + class_oid, + }): Path, +) -> Result { + // FIXME auth + let base = Base::with_id(base_id).fetch_one(&mut app_db).await?; + let lens = Lens::with_id(lens_id).fetch_one(&mut app_db).await?; + + let mut base_client = base_pooler + .acquire_for(lens.base_id, RoleAssignment::User(current_user.id)) + .await?; + let rel = PgClass::with_oid(lens.class_oid) + .fetch_one(&mut base_client) + .await?; + + let attrs = PgAttribute::all_for_rel(lens.class_oid) + .fetch_all(&mut base_client) + .await?; + let fields = Field::belonging_to_lens(lens.id) + .fetch_all(&mut app_db) + .await?; + let pkey_attrs = PgAttribute::pkeys_for_rel(lens.class_oid) + .fetch_all(&mut base_client) + .await?; + + const FRONTEND_ROW_LIMIT: i64 = 1000; + let rows: Vec = query(&format!( + "select {0} from {1}.{2} limit $1", + pkey_attrs + .iter() + .chain(attrs.iter()) + .map(|attr| escape_identifier(&attr.attname)) + .collect::>() + .join(", "), + escape_identifier(&rel.regnamespace), + escape_identifier(&rel.relname), + )) + .bind(FRONTEND_ROW_LIMIT) + .fetch_all(base_client.get_conn()) + .await?; + + #[derive(Serialize)] + struct DataRow { + pkey: String, + data: Vec, + } + + let mut data: Vec = vec![]; + let mut pkeys: Vec = vec![]; + for row in rows.iter() { + let mut pkey_values: HashMap = HashMap::new(); + for attr in pkey_attrs.clone() { + let field = Field::default_from_attr(&attr); + pkey_values.insert(field.name.clone(), field.get_value_encodable(row)?); + } + let pkey = serde_json::to_string(&pkey_values)?; + pkeys.push(pkey.clone()); + let mut row_data: Vec = vec![]; + for field in fields.iter() { + row_data.push(field.get_value_encodable(row)?); + } + data.push(DataRow { + pkey, + data: row_data, + }); + } + + #[derive(Serialize)] + struct ResponseBody { + pkeys: Vec, + data: Vec, + fields: Vec, + } + Ok(Json(ResponseBody { + fields, + pkeys, + data, + }) + .into_response()) +} + #[derive(Debug, Deserialize)] pub struct AddColumnPageForm { name: String, diff --git a/interim-server/templates/lens.html b/interim-server/templates/lens.html index 10ff3e0..3581028 100644 --- a/interim-server/templates/lens.html +++ b/interim-server/templates/lens.html @@ -13,7 +13,7 @@ {% for field in fields %} - + {{ field.label.clone().unwrap_or(field.name.clone()) }} {% endfor %} @@ -52,6 +52,23 @@ {% endfor %} {% endfor %} + + {% for (i, field) in fields.iter().enumerate() %} + + <{{ field.webc_tag() | safe }} + {% for (k, v) in field.webc_custom_attrs() %} + {{ k }}="{{ v }}" + {% endfor %} + row="{{ pkeys.len() }}" + column="{{ i }}" + class="cell" + insertable="true" + value="{{ field.field_type.default_for_insert()? | json }}" + > + + {% endfor %} + diff --git a/interim-server/templates/lens0_2.html b/interim-server/templates/lens0_2.html new file mode 100644 index 0000000..92d1628 --- /dev/null +++ b/interim-server/templates/lens0_2.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block main %} + +
+
+
+ {{ navbar | safe }} +
+
+ +
+
+ +{% endblock %} + diff --git a/mise.toml b/mise.toml index b40fd53..b1e1268 100644 --- a/mise.toml +++ b/mise.toml @@ -8,15 +8,21 @@ rust = { version = "1.88.0", components = "rust-analyzer,clippy" } watchexec = "latest" "github:sass/dart-sass" = "1.89.2" -[tasks.postgres] -run = "docker run --rm -it -e POSTGRES_PASSWORD=guest -v './pgdata:/var/lib/postgresql/data' -p 127.0.0.1:5432:5432 postgres:17" +[tasks.dev-services] +run = "docker compose up" +dir = "./dev-services" [tasks.server] run = "cargo run serve" description = "Run the server. For development: `mise watch --restart serve`." sources = ["**/*.rs", "**/*.html"] -[tasks.build-js] +[tasks.build-svelte] +run = "deno run -A npm:vite build" +dir = "./svelte" +sources = ["svelte/src/**/*.ts", "svelte/src/**/*.svelte"] + +[tasks.build-gleam] run = "sh build.sh" dir = "./webc" sources = ["webc/src/**/*.gleam", "webc/src/**/*.mjs"] diff --git a/sass/_globals.scss b/sass/_globals.scss index 4ad9cbe..3250e7b 100644 --- a/sass/_globals.scss +++ b/sass/_globals.scss @@ -59,6 +59,7 @@ $notice-color-info: #39d; font-family: inherit; font-size: inherit; font-weight: inherit; + outline: none; } @mixin rounded-sm { diff --git a/sass/main.scss b/sass/main.scss index fadb8e0..0a6860c 100644 --- a/sass/main.scss +++ b/sass/main.scss @@ -67,6 +67,7 @@ button, input[type="submit"] { &__main { grid-area: main; + overflow: hidden; } } diff --git a/sass/viewer.scss b/sass/viewer.scss index 29f4011..74519ea 100644 --- a/sass/viewer.scss +++ b/sass/viewer.scss @@ -1,34 +1,135 @@ -.viewer-table { - border-collapse: collapse; - height: 1px; // css hack to make percentage based cell heights work +@use 'globals'; +@use 'sass:color'; - &__column-header { - border: solid 1px #ccc; - border-top: none; - font-family: "Funnel Sans"; - background: #0001; - height: 100%; // css hack to make percentage based cell heights work - padding: 0.5rem; - text-align: left; +$table-border-color: #ccc; - &:first-child { - border-left: none; - } +.lens-grid { + display: grid; + grid-template: + 'table' 1fr + 'editor' max-content; + height: 100%; + width: 100%; +} + +.lens-table { + display: grid; + grid-area: table; + grid-template: + 'headers' max-content + 'main' 1fr + 'inserter' max-content; + height: 100%; + outline: none; + overflow: auto; + width: 100%; + + &__headers { + display: flex; + grid-area: headers; + align-items: stretch; } - &__actions-header { + &__header { + display: flex; + flex: none; + align-items: center; + border: solid 1px #ccc; + border-top: none; + border-left: none; + font-family: "Funnel Sans"; + background: #0001; + padding: 0.5rem; + text-align: left; + } + + &__header-actions { border: none; background: none; padding: 0; } - &__td { - border: solid 1px #ccc; - height: 100%; // css hack to make percentage based cell heights work + &__main { + grid-area: main; + overflow-y: auto; + } + + &__row { + align-items: stretch; + display: flex; + } + + &__cell { + flex: none; + border: solid 1px $table-border-color; + border-left: none; + border-top: none; padding: 0; - &:first-child { + &--insertable { + border-style: dashed; + } + + &--selected { + .lens-table__cell-content { + outline: 3px solid #37f; + outline-offset: -2px; + } + } + } + + &__cell-content { + font-family: globals.$font-family-data; + + &--null { + color: color.scale(#000, $lightness: 50%, $space: hsl); + font-style: oblique; + text-align: center; + padding: 0.5rem; + } + + &--text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0.5rem; + } + + &--uuid { + font-family: globals.$font-family-mono; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 0.5rem; + } + } + + &__inserter { + grid-area: inserter; + margin-bottom: 2rem; + + .lens-table__cell { + border: dashed 1px $table-border-color; border-left: none; + border-top: none; + } + + .lens-table__row:first-child .lens-table__cell { + border-top: dashed 1px $table-border-color; } } } + +.lens-editor { + align-items: stretch; + border-top: globals.$default-border; + display: flex; + grid-area: editor; + + &__input { + @include globals.reset_input; + padding: 0.5rem; + font-family: globals.$font-family-data; + flex: 1; + } +} diff --git a/svelte/.gitignore b/svelte/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/svelte/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/svelte/.npmrc b/svelte/.npmrc new file mode 100644 index 0000000..41583e3 --- /dev/null +++ b/svelte/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/svelte/README.md b/svelte/README.md new file mode 100644 index 0000000..e6cd94f --- /dev/null +++ b/svelte/README.md @@ -0,0 +1,47 @@ +# Svelte + TS + Vite + +This template should help get you started developing with Svelte and TypeScript in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). + +## Need an official Svelte framework? + +Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more. + +## Technical considerations + +**Why use this over SvelteKit?** + +- It brings its own routing solution which might not be preferable for some users. +- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app. + +This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project. + +Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate. + +**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?** + +Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information. + +**Why include `.vscode/extensions.json`?** + +Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project. + +**Why enable `allowJs` in the TS template?** + +While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant. + +**Why is HMR not preserving my local component state?** + +HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr). + +If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR. + +```ts +// store.ts +// An extremely simple external store +import { writable } from 'svelte/store' +export default writable(0) +``` diff --git a/svelte/deno.json b/svelte/deno.json new file mode 100644 index 0000000..5538e8e --- /dev/null +++ b/svelte/deno.json @@ -0,0 +1,11 @@ +{ + "tasks": { + "dev": "deno run --watch main.ts" + }, + "imports": { + "@std/assert": "jsr:@std/assert@1", + "@std/path": "jsr:@std/path@^1.1.1", + "@std/uuid": "jsr:@std/uuid@^1.0.9", + }, + "unstable": ["fmt-component"] +} diff --git a/svelte/deno.lock b/svelte/deno.lock new file mode 100644 index 0000000..73c4b79 --- /dev/null +++ b/svelte/deno.lock @@ -0,0 +1,1012 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/assert@1": "1.0.13", + "jsr:@std/bytes@^1.0.6": "1.0.6", + "jsr:@std/crypto@^1.0.5": "1.0.5", + "jsr:@std/internal@^1.0.6": "1.0.10", + "jsr:@std/internal@^1.0.9": "1.0.10", + "jsr:@std/path@^1.1.1": "1.1.1", + "jsr:@std/uuid@*": "1.0.9", + "jsr:@std/uuid@^1.0.9": "1.0.9", + "npm:@deno/vite-plugin@^1.0.5": "1.0.5_vite@7.1.2__picomatch@4.0.3", + "npm:@jsr/std__uuid@^1.0.9": "1.0.9", + "npm:@sveltejs/vite-plugin-svelte@^6.1.1": "6.1.1_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3", + "npm:@tsconfig/svelte@^5.0.4": "5.0.4", + "npm:svelte-check@^4.3.1": "4.3.1_svelte@5.38.1__acorn@8.15.0_typescript@5.8.3", + "npm:svelte-language-server@~0.17.19": "0.17.19_prettier@3.3.3_svelte@4.2.20_typescript@5.9.2", + "npm:svelte@^5.37.3": "5.38.1_acorn@8.15.0", + "npm:typescript@~5.8.3": "5.8.3", + "npm:uuid@^11.1.0": "11.1.0", + "npm:vite@^7.1.1": "7.1.2_picomatch@4.0.3", + "npm:zod@^4.0.17": "4.0.17" + }, + "jsr": { + "@std/assert@1.0.13": { + "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", + "dependencies": [ + "jsr:@std/internal@^1.0.6" + ] + }, + "@std/bytes@1.0.6": { + "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a" + }, + "@std/crypto@1.0.5": { + "integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40" + }, + "@std/internal@1.0.6": { + "integrity": "9533b128f230f73bd209408bb07a4b12f8d4255ab2a4d22a1fd6d87304aca9a4" + }, + "@std/internal@1.0.10": { + "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" + }, + "@std/path@1.1.1": { + "integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76", + "dependencies": [ + "jsr:@std/internal@^1.0.9" + ] + }, + "@std/uuid@1.0.9": { + "integrity": "44b627bf2d372fe1bd099e2ad41b2be41a777fc94e62a3151006895a037f1642", + "dependencies": [ + "jsr:@std/bytes", + "jsr:@std/crypto" + ] + } + }, + "npm": { + "@ampproject/remapping@2.3.0": { + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": [ + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping" + ] + }, + "@deno/vite-plugin@1.0.5_vite@7.1.2__picomatch@4.0.3": { + "integrity": "sha512-tLja5n4dyMhcze1NzvSs2iiriBymfBlDCZIrjMTxb9O2ru0gvmV6mn5oBD2teNw5Sd92cj3YJzKwsAs8tMJXlg==", + "dependencies": [ + "vite" + ] + }, + "@emmetio/abbreviation@2.3.3": { + "integrity": "sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==", + "dependencies": [ + "@emmetio/scanner" + ] + }, + "@emmetio/css-abbreviation@2.1.8": { + "integrity": "sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==", + "dependencies": [ + "@emmetio/scanner" + ] + }, + "@emmetio/scanner@1.0.4": { + "integrity": "sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==" + }, + "@esbuild/aix-ppc64@0.25.8": { + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@esbuild/aix-ppc64@0.25.9": { + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@esbuild/android-arm64@0.25.8": { + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@esbuild/android-arm64@0.25.9": { + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@esbuild/android-arm@0.25.8": { + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "os": ["android"], + "cpu": ["arm"] + }, + "@esbuild/android-arm@0.25.9": { + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "os": ["android"], + "cpu": ["arm"] + }, + "@esbuild/android-x64@0.25.8": { + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "os": ["android"], + "cpu": ["x64"] + }, + "@esbuild/android-x64@0.25.9": { + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "os": ["android"], + "cpu": ["x64"] + }, + "@esbuild/darwin-arm64@0.25.8": { + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@esbuild/darwin-arm64@0.25.9": { + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@esbuild/darwin-x64@0.25.8": { + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@esbuild/darwin-x64@0.25.9": { + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@esbuild/freebsd-arm64@0.25.8": { + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@esbuild/freebsd-arm64@0.25.9": { + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@esbuild/freebsd-x64@0.25.8": { + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@esbuild/freebsd-x64@0.25.9": { + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@esbuild/linux-arm64@0.25.8": { + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@esbuild/linux-arm64@0.25.9": { + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@esbuild/linux-arm@0.25.8": { + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@esbuild/linux-arm@0.25.9": { + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@esbuild/linux-ia32@0.25.8": { + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "os": ["linux"], + "cpu": ["ia32"] + }, + "@esbuild/linux-ia32@0.25.9": { + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "os": ["linux"], + "cpu": ["ia32"] + }, + "@esbuild/linux-loong64@0.25.8": { + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@esbuild/linux-loong64@0.25.9": { + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@esbuild/linux-mips64el@0.25.8": { + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@esbuild/linux-mips64el@0.25.9": { + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@esbuild/linux-ppc64@0.25.8": { + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@esbuild/linux-ppc64@0.25.9": { + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@esbuild/linux-riscv64@0.25.8": { + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@esbuild/linux-riscv64@0.25.9": { + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@esbuild/linux-s390x@0.25.8": { + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@esbuild/linux-s390x@0.25.9": { + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@esbuild/linux-x64@0.25.8": { + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@esbuild/linux-x64@0.25.9": { + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@esbuild/netbsd-arm64@0.25.8": { + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@esbuild/netbsd-arm64@0.25.9": { + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@esbuild/netbsd-x64@0.25.8": { + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@esbuild/netbsd-x64@0.25.9": { + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@esbuild/openbsd-arm64@0.25.8": { + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@esbuild/openbsd-arm64@0.25.9": { + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@esbuild/openbsd-x64@0.25.8": { + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@esbuild/openbsd-x64@0.25.9": { + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@esbuild/openharmony-arm64@0.25.8": { + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@esbuild/openharmony-arm64@0.25.9": { + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "os": ["openharmony"], + "cpu": ["arm64"] + }, + "@esbuild/sunos-x64@0.25.8": { + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@esbuild/sunos-x64@0.25.9": { + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@esbuild/win32-arm64@0.25.8": { + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@esbuild/win32-arm64@0.25.9": { + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@esbuild/win32-ia32@0.25.8": { + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@esbuild/win32-ia32@0.25.9": { + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@esbuild/win32-x64@0.25.8": { + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@esbuild/win32-x64@0.25.9": { + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@jridgewell/gen-mapping@0.3.13": { + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": [ + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/remapping@2.3.5": { + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dependencies": [ + "@jridgewell/gen-mapping", + "@jridgewell/trace-mapping" + ] + }, + "@jridgewell/resolve-uri@3.1.2": { + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==" + }, + "@jridgewell/sourcemap-codec@1.5.5": { + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "@jridgewell/trace-mapping@0.3.30": { + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dependencies": [ + "@jridgewell/resolve-uri", + "@jridgewell/sourcemap-codec" + ] + }, + "@jsr/std__bytes@1.0.6": { + "integrity": "sha512-St6yKggjFGhxS52IFLJWvkchRFbAKg2Xh8UxA4S1EGz7GJ2Ui+ssDDldj/w2c8vCxvl6qgR0HaYbKeFJNqujmA==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__bytes/1.0.6.tgz" + }, + "@jsr/std__crypto@1.0.5": { + "integrity": "sha512-iqFCkjeGeQccLgmxH9m1d7abjZcFMW0XrYZu1itNz8vVHzH9crObalonjVQaVDdKHCrNNOklMN1t0u3k46dirA==", + "tarball": "https://npm.jsr.io/~/11/@jsr/std__crypto/1.0.5.tgz" + }, + "@jsr/std__uuid@1.0.9": { + "integrity": "sha512-hyTTOsmUTnLuah/OMnkYfZ8fSqYbB113idHZfIYlhN26PdpMGTqTUZkFCA1Q6oBhW8d8N82RG0PnRohQ0peqhA==", + "dependencies": [ + "@jsr/std__bytes", + "@jsr/std__crypto" + ], + "tarball": "https://npm.jsr.io/~/11/@jsr/std__uuid/1.0.9.tgz" + }, + "@rollup/rollup-android-arm-eabi@4.46.2": { + "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==", + "os": ["android"], + "cpu": ["arm"] + }, + "@rollup/rollup-android-arm64@4.46.2": { + "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-arm64@4.46.2": { + "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@rollup/rollup-darwin-x64@4.46.2": { + "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@rollup/rollup-freebsd-arm64@4.46.2": { + "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@rollup/rollup-freebsd-x64@4.46.2": { + "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-arm-gnueabihf@4.46.2": { + "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm-musleabihf@4.46.2": { + "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@rollup/rollup-linux-arm64-gnu@4.46.2": { + "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-arm64-musl@4.46.2": { + "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@rollup/rollup-linux-loongarch64-gnu@4.46.2": { + "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@rollup/rollup-linux-ppc64-gnu@4.46.2": { + "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@rollup/rollup-linux-riscv64-gnu@4.46.2": { + "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-riscv64-musl@4.46.2": { + "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@rollup/rollup-linux-s390x-gnu@4.46.2": { + "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@rollup/rollup-linux-x64-gnu@4.46.2": { + "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-linux-x64-musl@4.46.2": { + "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@rollup/rollup-win32-arm64-msvc@4.46.2": { + "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@rollup/rollup-win32-ia32-msvc@4.46.2": { + "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@rollup/rollup-win32-x64-msvc@4.46.2": { + "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@sveltejs/acorn-typescript@1.0.5_acorn@8.15.0": { + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "dependencies": [ + "acorn" + ] + }, + "@sveltejs/vite-plugin-svelte-inspector@5.0.0_@sveltejs+vite-plugin-svelte@6.1.1__svelte@5.38.1___acorn@8.15.0__vite@7.1.2___picomatch@4.0.3_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3": { + "integrity": "sha512-iwQ8Z4ET6ZFSt/gC+tVfcsSBHwsqc6RumSaiLUkAurW3BCpJam65cmHw0oOlDMTO0u+PZi9hilBRYN+LZNHTUQ==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte", + "debug", + "svelte@5.38.1_acorn@8.15.0", + "vite" + ] + }, + "@sveltejs/vite-plugin-svelte@6.1.1_svelte@5.38.1__acorn@8.15.0_vite@7.1.2__picomatch@4.0.3": { + "integrity": "sha512-vB0Vq47Js7C11L2JrwhncIAoDNkdKDPI500SjLSb34X48dDcsSH5JpLl0cHT0sfO997BrzAS6PKjiZEey/S0VQ==", + "dependencies": [ + "@sveltejs/vite-plugin-svelte-inspector", + "debug", + "deepmerge", + "kleur", + "magic-string", + "svelte@5.38.1_acorn@8.15.0", + "vite", + "vitefu" + ] + }, + "@tsconfig/svelte@5.0.4": { + "integrity": "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q==" + }, + "@types/estree@1.0.8": { + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, + "@vscode/emmet-helper@2.8.4": { + "integrity": "sha512-lUki5QLS47bz/U8IlG9VQ+1lfxMtxMZENmU5nu4Z71eOD5j9FK0SmYGL5NiVJg9WBWeAU0VxRADMY2Qpq7BfVg==", + "dependencies": [ + "emmet", + "jsonc-parser", + "vscode-languageserver-textdocument", + "vscode-languageserver-types", + "vscode-nls", + "vscode-uri@2.1.2" + ] + }, + "@vscode/l10n@0.0.18": { + "integrity": "sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==" + }, + "acorn@8.15.0": { + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "bin": true + }, + "aria-query@5.3.2": { + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" + }, + "axobject-query@4.1.0": { + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==" + }, + "chokidar@4.0.3": { + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dependencies": [ + "readdirp" + ] + }, + "clsx@2.1.1": { + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "code-red@1.0.4": { + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dependencies": [ + "@jridgewell/sourcemap-codec", + "@types/estree", + "acorn", + "estree-walker@3.0.3", + "periscopic" + ] + }, + "css-tree@2.3.1": { + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": [ + "mdn-data", + "source-map-js" + ] + }, + "debug@4.4.1": { + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": [ + "ms" + ] + }, + "dedent-js@1.0.1": { + "integrity": "sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==" + }, + "deepmerge@4.3.1": { + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==" + }, + "emmet@2.4.11": { + "integrity": "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==", + "dependencies": [ + "@emmetio/abbreviation", + "@emmetio/css-abbreviation" + ] + }, + "esbuild@0.25.9": { + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "optionalDependencies": [ + "@esbuild/aix-ppc64@0.25.9", + "@esbuild/android-arm@0.25.9", + "@esbuild/android-arm64@0.25.9", + "@esbuild/android-x64@0.25.9", + "@esbuild/darwin-arm64@0.25.9", + "@esbuild/darwin-x64@0.25.9", + "@esbuild/freebsd-arm64@0.25.9", + "@esbuild/freebsd-x64@0.25.9", + "@esbuild/linux-arm@0.25.9", + "@esbuild/linux-arm64@0.25.9", + "@esbuild/linux-ia32@0.25.9", + "@esbuild/linux-loong64@0.25.9", + "@esbuild/linux-mips64el@0.25.9", + "@esbuild/linux-ppc64@0.25.9", + "@esbuild/linux-riscv64@0.25.9", + "@esbuild/linux-s390x@0.25.9", + "@esbuild/linux-x64@0.25.9", + "@esbuild/netbsd-arm64@0.25.9", + "@esbuild/netbsd-x64@0.25.9", + "@esbuild/openbsd-arm64@0.25.9", + "@esbuild/openbsd-x64@0.25.9", + "@esbuild/openharmony-arm64@0.25.9", + "@esbuild/sunos-x64@0.25.9", + "@esbuild/win32-arm64@0.25.9", + "@esbuild/win32-ia32@0.25.9", + "@esbuild/win32-x64@0.25.9" + ], + "scripts": true, + "bin": true + }, + "esm-env@1.2.2": { + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" + }, + "esrap@2.1.0": { + "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==", + "dependencies": [ + "@jridgewell/sourcemap-codec" + ] + }, + "estree-walker@2.0.2": { + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "estree-walker@3.0.3": { + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": [ + "@types/estree" + ] + }, + "fdir@6.4.6_picomatch@4.0.3": { + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dependencies": [ + "picomatch" + ], + "optionalPeers": [ + "picomatch" + ] + }, + "fsevents@2.3.3": { + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "os": ["darwin"], + "scripts": true + }, + "globrex@0.1.2": { + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, + "is-reference@3.0.3": { + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dependencies": [ + "@types/estree" + ] + }, + "jsonc-parser@2.3.1": { + "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==" + }, + "kleur@4.1.5": { + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" + }, + "locate-character@3.0.0": { + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" + }, + "lodash@4.17.21": { + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lower-case@2.0.2": { + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dependencies": [ + "tslib" + ] + }, + "magic-string@0.30.17": { + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dependencies": [ + "@jridgewell/sourcemap-codec" + ] + }, + "mdn-data@2.0.30": { + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, + "mri@1.2.0": { + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" + }, + "ms@2.1.3": { + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "nanoid@3.3.11": { + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "bin": true + }, + "no-case@3.0.4": { + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dependencies": [ + "lower-case", + "tslib" + ] + }, + "pascal-case@3.1.2": { + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dependencies": [ + "no-case", + "tslib" + ] + }, + "periscopic@3.1.0": { + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dependencies": [ + "@types/estree", + "estree-walker@3.0.3", + "is-reference" + ] + }, + "picocolors@1.1.1": { + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch@4.0.3": { + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==" + }, + "postcss@8.5.6": { + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + "prettier-plugin-svelte@3.4.0_prettier@3.3.3_svelte@4.2.20": { + "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", + "dependencies": [ + "prettier", + "svelte@4.2.20" + ] + }, + "prettier@3.3.3": { + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "bin": true + }, + "readdirp@4.1.2": { + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" + }, + "rollup@4.46.2": { + "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==", + "dependencies": [ + "@types/estree" + ], + "optionalDependencies": [ + "@rollup/rollup-android-arm-eabi", + "@rollup/rollup-android-arm64", + "@rollup/rollup-darwin-arm64", + "@rollup/rollup-darwin-x64", + "@rollup/rollup-freebsd-arm64", + "@rollup/rollup-freebsd-x64", + "@rollup/rollup-linux-arm-gnueabihf", + "@rollup/rollup-linux-arm-musleabihf", + "@rollup/rollup-linux-arm64-gnu", + "@rollup/rollup-linux-arm64-musl", + "@rollup/rollup-linux-loongarch64-gnu", + "@rollup/rollup-linux-ppc64-gnu", + "@rollup/rollup-linux-riscv64-gnu", + "@rollup/rollup-linux-riscv64-musl", + "@rollup/rollup-linux-s390x-gnu", + "@rollup/rollup-linux-x64-gnu", + "@rollup/rollup-linux-x64-musl", + "@rollup/rollup-win32-arm64-msvc", + "@rollup/rollup-win32-ia32-msvc", + "@rollup/rollup-win32-x64-msvc", + "fsevents" + ], + "bin": true + }, + "sade@1.8.1": { + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": [ + "mri" + ] + }, + "semver@7.7.2": { + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "bin": true + }, + "source-map-js@1.2.1": { + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "svelte-check@4.3.1_svelte@5.38.1__acorn@8.15.0_typescript@5.8.3": { + "integrity": "sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg==", + "dependencies": [ + "@jridgewell/trace-mapping", + "chokidar", + "fdir", + "picocolors", + "sade", + "svelte@5.38.1_acorn@8.15.0", + "typescript@5.8.3" + ], + "bin": true + }, + "svelte-language-server@0.17.19_prettier@3.3.3_svelte@4.2.20_typescript@5.9.2": { + "integrity": "sha512-Zccae9GAcgJQVAiQUcUmjM9KHuydWeMshbsXVt99MKX94fDx+XAylSxvhSgiTyEqYwxmbta1mFSS2Fv3YcJB7w==", + "dependencies": [ + "@jridgewell/trace-mapping", + "@vscode/emmet-helper", + "chokidar", + "estree-walker@2.0.2", + "fdir", + "globrex", + "lodash", + "prettier", + "prettier-plugin-svelte", + "svelte@4.2.20", + "svelte2tsx", + "typescript@5.9.2", + "typescript-auto-import-cache", + "vscode-css-languageservice", + "vscode-html-languageservice", + "vscode-languageserver", + "vscode-languageserver-protocol", + "vscode-languageserver-types", + "vscode-uri@3.1.0" + ], + "bin": true + }, + "svelte2tsx@0.7.42_svelte@4.2.20_typescript@5.9.2": { + "integrity": "sha512-PSNrKS16aVdAajoFjpF5M0t6TA7ha7GcKbBajD9RG3M+vooAuvLnWAGUSC6eJL4zEOVbOWKtcS2BuY4rxPljoA==", + "dependencies": [ + "dedent-js", + "pascal-case", + "svelte@4.2.20", + "typescript@5.9.2" + ] + }, + "svelte@4.2.20": { + "integrity": "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==", + "dependencies": [ + "@ampproject/remapping", + "@jridgewell/sourcemap-codec", + "@jridgewell/trace-mapping", + "@types/estree", + "acorn", + "aria-query", + "axobject-query", + "code-red", + "css-tree", + "estree-walker@3.0.3", + "is-reference", + "locate-character", + "magic-string", + "periscopic" + ] + }, + "svelte@5.38.1_acorn@8.15.0": { + "integrity": "sha512-fO6CLDfJYWHgfo6lQwkQU2vhCiHc2MBl6s3vEhK+sSZru17YL4R5s1v14ndRpqKAIkq8nCz6MTk1yZbESZWeyQ==", + "dependencies": [ + "@jridgewell/remapping", + "@jridgewell/sourcemap-codec", + "@sveltejs/acorn-typescript", + "@types/estree", + "acorn", + "aria-query", + "axobject-query", + "clsx", + "esm-env", + "esrap", + "is-reference", + "locate-character", + "magic-string", + "zimmerframe" + ] + }, + "tinyglobby@0.2.14_picomatch@4.0.3": { + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dependencies": [ + "fdir", + "picomatch" + ] + }, + "tslib@2.8.1": { + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "typescript-auto-import-cache@0.3.6": { + "integrity": "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==", + "dependencies": [ + "semver" + ] + }, + "typescript@5.8.3": { + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "bin": true + }, + "typescript@5.9.2": { + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "bin": true + }, + "uuid@11.1.0": { + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "bin": true + }, + "vite@7.1.2_picomatch@4.0.3": { + "integrity": "sha512-J0SQBPlQiEXAF7tajiH+rUooJPo0l8KQgyg4/aMunNtrOa7bwuZJsJbDWzeljqQpgftxuq5yNJxQ91O9ts29UQ==", + "dependencies": [ + "esbuild", + "fdir", + "picomatch", + "postcss", + "rollup", + "tinyglobby" + ], + "optionalDependencies": [ + "fsevents" + ], + "bin": true + }, + "vitefu@1.1.1_vite@7.1.2__picomatch@4.0.3": { + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", + "dependencies": [ + "vite" + ], + "optionalPeers": [ + "vite" + ] + }, + "vscode-css-languageservice@6.3.7": { + "integrity": "sha512-5TmXHKllPzfkPhW4UE9sODV3E0bIOJPOk+EERKllf2SmAczjfTmYeq5txco+N3jpF8KIZ6loj/JptpHBQuVQRA==", + "dependencies": [ + "@vscode/l10n", + "vscode-languageserver-textdocument", + "vscode-languageserver-types", + "vscode-uri@3.1.0" + ] + }, + "vscode-html-languageservice@5.4.0": { + "integrity": "sha512-9/cbc90BSYCghmHI7/VbWettHZdC7WYpz2g5gBK6UDUI1MkZbM773Q12uAYJx9jzAiNHPpyo6KzcwmcnugncAQ==", + "dependencies": [ + "@vscode/l10n", + "vscode-languageserver-textdocument", + "vscode-languageserver-types", + "vscode-uri@3.1.0" + ] + }, + "vscode-jsonrpc@8.2.0": { + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==" + }, + "vscode-languageserver-protocol@3.17.5": { + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dependencies": [ + "vscode-jsonrpc", + "vscode-languageserver-types" + ] + }, + "vscode-languageserver-textdocument@1.0.12": { + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==" + }, + "vscode-languageserver-types@3.17.5": { + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==" + }, + "vscode-languageserver@9.0.1": { + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dependencies": [ + "vscode-languageserver-protocol" + ], + "bin": true + }, + "vscode-nls@5.2.0": { + "integrity": "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==" + }, + "vscode-uri@2.1.2": { + "integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==" + }, + "vscode-uri@3.1.0": { + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==" + }, + "zimmerframe@1.1.2": { + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" + }, + "zod@4.0.17": { + "integrity": "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ==" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@1", + "jsr:@std/path@^1.1.1", + "jsr:@std/uuid@^1.0.9" + ], + "packageJson": { + "dependencies": [ + "npm:@deno/vite-plugin@^1.0.5", + "npm:@jsr/std__uuid@^1.0.9", + "npm:@sveltejs/vite-plugin-svelte@^6.1.1", + "npm:@tsconfig/svelte@^5.0.4", + "npm:svelte-check@^4.3.1", + "npm:svelte-language-server@~0.17.19", + "npm:svelte@^5.37.3", + "npm:typescript@~5.8.3", + "npm:uuid@^11.1.0", + "npm:vite@^7.1.1", + "npm:zod@^4.0.17" + ] + } + } +} diff --git a/svelte/index.html b/svelte/index.html new file mode 100644 index 0000000..b6c5f0a --- /dev/null +++ b/svelte/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + Svelte + TS + + +
+ + + diff --git a/svelte/main.ts b/svelte/main.ts new file mode 100644 index 0000000..292ce5f --- /dev/null +++ b/svelte/main.ts @@ -0,0 +1,8 @@ +export function add(a: number, b: number): number { + return a + b; +} + +// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts +if (import.meta.main) { + console.log("Add 2 + 3 =", add(2, 3)); +} diff --git a/svelte/main_test.ts b/svelte/main_test.ts new file mode 100644 index 0000000..3d981e9 --- /dev/null +++ b/svelte/main_test.ts @@ -0,0 +1,6 @@ +import { assertEquals } from "@std/assert"; +import { add } from "./main.ts"; + +Deno.test(function addTest() { + assertEquals(add(2, 3), 5); +}); diff --git a/svelte/package.json b/svelte/package.json new file mode 100644 index 0000000..bcd2a5a --- /dev/null +++ b/svelte/package.json @@ -0,0 +1,27 @@ +{ + "name": "vite-project", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json" + }, + "dependencies": { + "@deno/vite-plugin": "^1.0.5", + "@jsr/std__uuid": "^1.0.9", + "@sveltejs/vite-plugin-svelte": "^6.1.1", + "svelte-language-server": "^0.17.19", + "uuid": "^11.1.0", + "vite": "^7.1.1", + "zod": "^4.0.17" + }, + "devDependencies": { + "@tsconfig/svelte": "^5.0.4", + "svelte": "^5.37.3", + "svelte-check": "^4.3.1", + "typescript": "~5.8.3" + } +} diff --git a/svelte/public/vite.svg b/svelte/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/svelte/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svelte/src/App.svelte b/svelte/src/App.svelte new file mode 100644 index 0000000..f75b68a --- /dev/null +++ b/svelte/src/App.svelte @@ -0,0 +1,47 @@ + + +
+ +

Vite + Svelte

+ +
+ +
+ +

+ Check out SvelteKit, the official Svelte app framework powered by Vite! +

+ +

+ Click on the Vite and Svelte logos to learn more +

+
+ + diff --git a/svelte/src/TableViewer.svelte b/svelte/src/TableViewer.svelte new file mode 100644 index 0000000..a6ccc41 --- /dev/null +++ b/svelte/src/TableViewer.svelte @@ -0,0 +1,533 @@ + + + + +{#snippet table_region({ + region_name, + rows, + on_cell_click, +}: { + region_name: string; + rows: Row[]; + on_cell_click(ev: MouseEvent, coords: Coords): void; +})} + {#if lazy_data} + {#each rows as row, row_index} +
+ {#each lazy_data.fields as field, field_index} + {@const cell_data = row.data[field_index]} + {@const cell_coords: Coords = [row_index, field_index]} + {@const cell_selected = selections.some( + (sel) => + sel.region === region_name && coords_eq(sel.coords, cell_coords), + )} + +
on_cell_click(ev, cell_coords)} + ondblclick={() => handle_table_cell_dblclick(cell_coords)} + role="gridcell" + style:width={`${field.width_px}px`} + tabindex="-1" + > + {#if cell_data.t === "Text"} +
+ {cell_data.c ?? "Null"} +
+ {:else if cell_data.t === "Uuid"} +
+ {cell_data.c ?? "Null"} +
+ {:else} +
+ UNKNOWN +
+ {/if} +
+ {/each} +
+ {/each} + {/if} +{/snippet} + +
+ {#if lazy_data} +
+
+ {#each lazy_data.fields as field, field_index} +
+
{field.label ?? field.name}
+
+ {/each} +
TODO
+
+
+ {@render table_region({ + region_name: "main", + rows: lazy_data.rows, + on_cell_click: handle_main_cell_click, + })} +
+
+ {@render table_region({ + region_name: "inserter", + rows: inserter_rows, + on_cell_click: handle_inserter_cell_click, + })} +
+
+
+ +
+ {/if} +
diff --git a/svelte/src/app.css b/svelte/src/app.css new file mode 100644 index 0000000..61ba367 --- /dev/null +++ b/svelte/src/app.css @@ -0,0 +1,79 @@ +:root { + font-family: 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; +} + +.card { + padding: 2em; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +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; + } +} diff --git a/svelte/src/assets/svelte.svg b/svelte/src/assets/svelte.svg new file mode 100644 index 0000000..c5e0848 --- /dev/null +++ b/svelte/src/assets/svelte.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/svelte/src/field.svelte.ts b/svelte/src/field.svelte.ts new file mode 100644 index 0000000..30e0115 --- /dev/null +++ b/svelte/src/field.svelte.ts @@ -0,0 +1,75 @@ +import { z } from "zod"; + +// -------- Encodable -------- // + +const encodable_text_schema = z.object({ + t: z.literal("Text"), + c: z.string().nullish().transform((x) => x ?? undefined), +}); + +const encodable_timestamp_schema = z.object({ + t: z.literal("Timestamp"), + c: z.coerce.date().nullish().transform((x) => x ?? undefined), +}); + +const encodable_uuid_schema = z.object({ + t: z.literal("Uuid"), + c: z.string().nullish().transform((x) => x ?? undefined), +}); + +const encodable_schema = z.union([ + encodable_text_schema, + encodable_timestamp_schema, + encodable_uuid_schema, +]); + +export type Encodable = z.infer; + +// -------- FieldType -------- // + +const integer_field_type_schema = z.object({ + t: z.literal("Integer"), + c: z.unknown(), +}); + +const text_field_type_schema = z.object({ + t: "Text", + c: z.unknown(), +}); + +const uuid_field_type_schema = z.object({ + t: "Uuid", + c: z.unknown(), +}); + +export const field_type_schema = z.union([ + integer_field_type_schema, + text_field_type_schema, + uuid_field_type_schema, +]); + +export type FieldType = z.infer; + +// -------- Field -------- // + +export type Field = { + id: string; + name: string; + label?: string; + field_type: FieldType; + width_px: number; +}; + +// -------- Table Utils -------- // +// TODO move this to its own module + +export type Coords = [number, number]; + +export type Row = { + key: string | number; + data: Encodable[]; +}; + +export function coords_eq(a: Coords, b: Coords): boolean { + return a[0] === b[0] && a[1] === b[1]; +} diff --git a/svelte/src/interactive-table.svelte b/svelte/src/interactive-table.svelte new file mode 100644 index 0000000..aac6504 --- /dev/null +++ b/svelte/src/interactive-table.svelte @@ -0,0 +1,122 @@ + + +
+ {#if show_headers} +
+ {#each fields as field, field_index} +
+
{field.label ?? field.name}
+
+ {/each} +
+ TODO +
+
+ {/if} +
+ {#each rows as row, row_index} +
+ {#each fields as field, field_index} + {@const cell_data = row.data[field_index]} + {@const cell_coords: Coords = [row_index, field_index]} + {@const cell_selected = selections.some( + (sel_coords) => coords_eq(sel_coords, cell_coords), + )} + +
on_cell_click?.(ev, cell_coords)} + ondblclick={() => on_cell_dblclick?.(cell_coords)} + role="gridcell" + style:width={`${field.width_px}px`} + tabindex="-1" + > + {#if cell_data.t === "Text"} +
+ {cell_data.c} +
+ {:else if cell_data.t === "Uuid"} +
+ {cell_data.c} +
+ {:else} +
+ UNKNOWN +
+ {/if} +
+ {/each} +
+ {/each} +
+
diff --git a/svelte/src/lib/Counter.svelte b/svelte/src/lib/Counter.svelte new file mode 100644 index 0000000..37d75ce --- /dev/null +++ b/svelte/src/lib/Counter.svelte @@ -0,0 +1,10 @@ + + + diff --git a/svelte/src/main.ts b/svelte/src/main.ts new file mode 100644 index 0000000..664a057 --- /dev/null +++ b/svelte/src/main.ts @@ -0,0 +1,9 @@ +import { mount } from 'svelte' +import './app.css' +import App from './App.svelte' + +const app = mount(App, { + target: document.getElementById('app')!, +}) + +export default app diff --git a/svelte/src/table-viewer/loaded.svelte b/svelte/src/table-viewer/loaded.svelte new file mode 100644 index 0000000..c39946a --- /dev/null +++ b/svelte/src/table-viewer/loaded.svelte @@ -0,0 +1,168 @@ + + + + +{#await data_promise} +
Loading...
+{:then data} +
+
+ {#each data.field_names as field_name, field_index} +
+
{data.fields[field_name].label ?? field_name}
+
+ {/each} +
+ TODO +
+
+
+ {#each data.pkeys as pkey, row_index} +
+ {#each data.field_names as field_name, field_index} + {@const cell_data = data.data[pkey][field_name]} + {@const selected = selections.some( + ([sel_pkey, sel_field]) => sel_pkey === pkey && sel_field === field_name, + )} + +
handle_click(ev, [pkey, field_name], cell_data)} + ondblclick={() => handle_dblclick([pkey, field_name])} + role="gridcell" + style:width={`${data.fields[field_name].width_px}px`} + tabindex="-1" + > + {#if cell_data.t === "Text"} +
coords_eq(coords, [pkey, field_name])) && "viewer-cell__content--selected", + ]} + > + {cell_data.c} +
+ {:else if cell_data.t === "Uuid"} +
{cell_data.c}
+ {:else} +
Unknown type
+ {/if} +
+ {/each} +
+ {/each} +
+
+
+ { + editing = true; + }} + role="combobox" + bind:this={editor_input_element} + bind:value={editor_input_value} + /> +
+{:catch err} + Error {err} +{/await} diff --git a/svelte/src/vite-env.d.ts b/svelte/src/vite-env.d.ts new file mode 100644 index 0000000..4078e74 --- /dev/null +++ b/svelte/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/svelte/svelte.config.js b/svelte/svelte.config.js new file mode 100644 index 0000000..8e11ff4 --- /dev/null +++ b/svelte/svelte.config.js @@ -0,0 +1,8 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; + +export default { + compilerOptions: { customElement: true }, + // Consult https://svelte.dev/docs#compile-time-svelte-preprocess + // for more information about preprocessors + preprocess: vitePreprocess(), +}; diff --git a/svelte/tsconfig.app.json b/svelte/tsconfig.app.json new file mode 100644 index 0000000..d9387fd --- /dev/null +++ b/svelte/tsconfig.app.json @@ -0,0 +1,20 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "module": "ESNext", + "resolveJsonModule": true, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "isolatedModules": true, + "moduleDetection": "force" + }, + "include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"] +} diff --git a/svelte/tsconfig.json b/svelte/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/svelte/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/svelte/tsconfig.node.json b/svelte/tsconfig.node.json new file mode 100644 index 0000000..f85a399 --- /dev/null +++ b/svelte/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/svelte/vite.config.ts b/svelte/vite.config.ts new file mode 100644 index 0000000..fcc372a --- /dev/null +++ b/svelte/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "vite"; +import * as path from "@std/path"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [svelte()], + build: { + rollupOptions: { + input: path.fromFileUrl( + new URL("./src/TableViewer.svelte", import.meta.url), + ), + output: { + dir: path.fromFileUrl(new URL("../js_dist", import.meta.url)), + entryFileNames: "[name].js", + chunkFileNames: "[name].js", + assetFileNames: "[name].[ext]", + }, + }, + }, +}); diff --git a/webc/src/cell_common.gleam b/webc/src/cell_common.gleam index 3109374..5f151c0 100644 --- a/webc/src/cell_common.gleam +++ b/webc/src/cell_common.gleam @@ -27,11 +27,23 @@ pub fn options_common( component.on_attribute_change("selected", fn(value) { ParentChangedSelected(value != "") |> CommonMsg |> map_msg |> Ok }), + component.on_attribute_change("insertable", fn(value) { + ParentChangedInsertable(value != "") |> CommonMsg |> map_msg |> Ok + }), + component.on_attribute_change("has-default", fn(value) { + ParentChangedInsertable(value != "") |> CommonMsg |> map_msg |> Ok + }), ] } pub type ModelCommon { - ModelCommon(root_path: String, row: Int, column: Int, selected: Bool) + ModelCommon( + root_path: String, + row: Int, + column: Int, + selected: Bool, + insertable: Bool, + ) } pub type Msg { @@ -39,13 +51,20 @@ pub type Msg { ParentChangedRow(Int) ParentChangedColumn(Int) ParentChangedSelected(Bool) + ParentChangedInsertable(Bool) UserClickedCell UserDoubleClickedCell } pub fn init(_) -> #(ModelCommon, Effect(CommonMsg)) { #( - ModelCommon(root_path: "", selected: False, row: -1, column: -1), + ModelCommon( + root_path: "", + selected: False, + row: -1, + column: -1, + insertable: False, + ), context.request_context( context: dynamic.string("root_path"), subscribe: False, @@ -79,6 +98,10 @@ pub fn update( ModelCommon(..model, selected:), effect.none(), ) + ParentChangedInsertable(insertable) -> #( + ModelCommon(..model, insertable:), + effect.none(), + ) UserClickedCell -> #( model, event.emit( diff --git a/webc/src/cell_uuid_component.gleam b/webc/src/cell_uuid_component.gleam index 2980c8d..e9a8ae0 100644 --- a/webc/src/cell_uuid_component.gleam +++ b/webc/src/cell_uuid_component.gleam @@ -34,13 +34,13 @@ pub fn component() -> App(Nil, Model, Msg) { } pub type Model { - Model(common: ModelCommon, value: Option(String)) + Model(common: ModelCommon, value: Option(String), has_default: Bool) } fn init(_) -> #(Model, Effect(Msg)) { let #(model_common, effect_common) = cell_common.init(Nil) #( - Model(common: model_common, value: option.None), + Model(common: model_common, value: None, has_default: False), effect_common |> effect.map(fn(effect_common) { Common(effect_common) }), ) @@ -71,7 +71,17 @@ fn view(model: Model) -> Element(Msg) { False -> attr.class("cell__content--uuid") }, ], - [html.text(model.value |> option.unwrap("Null"))], + [ + html.text( + model.value + |> option.unwrap( + case model.common.insertable && model.common.has_default { + True -> "Default" + False -> "Null" + }, + ), + ), + ], ) }) } diff --git a/webc/src/history.gleam b/webc/src/history.gleam new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/webc/src/history.gleam @@ -0,0 +1 @@ + diff --git a/webc/src/table_viewer_component.gleam b/webc/src/table_viewer_component.gleam new file mode 100644 index 0000000..bcde1fe --- /dev/null +++ b/webc/src/table_viewer_component.gleam @@ -0,0 +1,465 @@ +import gleam/dict.{type Dict} +import gleam/dynamic.{type Dynamic} +import gleam/dynamic/decode +import gleam/http/response.{type Response} +import gleam/io +import gleam/json +import gleam/list +import gleam/option.{None, Some} +import gleam/pair +import gleam/regexp +import gleam/result +import gleam/set.{type Set} +import lustre.{type App} +import lustre/component +import lustre/effect.{type Effect} +import lustre/element.{type Element} +import lustre/element/html +import lustre/event +import plinth/browser/document +import plinth/browser/event as plinth_event +import rsvp + +import context +import encodable.{type Encodable} +import field.{type Field} +import field_type.{type FieldType} + +pub const name: String = "table-viewer" + +pub fn component() -> App(Nil, Model, Msg) { + lustre.component(init, update, view, [ + component.on_attribute_change("root-path", fn(value) { + ParentChangedRootPath(value) |> Ok + }), + ]) +} + +// -------- Model -------- // + +type Coord { + Coord(pkey: String, field: String) +} + +type Cell { + Cell(value_committed: Encodable, value_current: Encodable) +} + +pub type Model { + Model( + root_path: String, + selections: List(Coord), + editing: Bool, + fields: Dict(String, Field), + field_names: List(String), + history: List(HistoryEvent), + pkeys: List(String), + data: Dict(Coord, Encodable), + // Whether OS, ctrl, or alt keys are being held. If so, simultaneously + // pressing a typing key should not trigger an overwrite. + modifiers_held: Set(String), + ) +} + +pub type HistoryEvent { + CommitCellValue(from: Encodable, to: Encodable) +} + +pub fn apply_history(ev: HistoryEvent) -> Effect(Msg) { + case ev { + CommitCellValue -> { + todo + } + } +} + +fn init(_) -> #(Model, Effect(Msg)) { + #( + Model( + root_path: "", + selections: [], + editing: False, + fields: dict.new(), + field_names: [], + pkeys: [], + values: dict.new(), + modifiers_held: set.new(), + ), + effect.before_paint(fn(dispatch, _) -> Nil { + document.add_event_listener("keydown", fn(ev) -> Nil { + let key = plinth_event.key(ev) + case key { + "ArrowLeft" | "ArrowRight" | "ArrowUp" | "ArrowDown" -> + dispatch(UserPressedDirectionalKey(key)) + "Enter" -> dispatch(UserPressedEnterKey) + "Shift", "Meta" | "Alt" | "Control" -> + dispatch(UserPressedModifierKey(key)) + _ -> + case is_typing_key(key) { + True -> dispatch(UserPressedTypingKey(key)) + False -> Nil + } + } + }) + document.add_event_listener("keyup", fn(ev) -> Nil { + let key = plinth_event.key(ev) + case key { + "Meta" | "Alt" | "Control" -> dispatch(UserReleasedModifierKey(key)) + _ -> Nil + } + }) + }), + ) +} + +fn is_typing_key(key: String) -> Bool { + let assert Ok(re) = + regexp.from_string("^[a-zA-Z0-9!@#$%^&*._/?<>{}[\\]'\"~`-]$") + regexp.check(key, with: re) +} + +// -------- Update -------- // + +pub type EditorMsg { + EditStarted + EditStateChanged(EditState) + EditFinished(Encodable) +} + +pub type ApiMsg { + CellValueCommitted(Coord) + RowInserted +} + +pub type MouseMsg { + UserClickedCell(Coord) + UserDoubleClickedCell(Coord) +} + +pub type KeyboardMsg { + UserPressedDirectionalKey(String) + UserPressedEnterKey + UserPressedTypingKey(String) + UserPressedModifierKey(String) + UserReleasedModifierKey(String) +} + +pub type Msg { + ParentChangedRootPath(String) + EditorMessage(EditorMsg) + ApiMessage(ApiMsg) + MouseMessage(MouseMsg) + KeyboardMessage(KeyboardMsg) +} + +fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { + case msg { + ParentChangedRootPath(root_path) -> #( + Model(..model, root_path:), + context.update_consumers( + model.root_path_consumers, + dynamic.string(root_path), + ), + ) + EditorMessage(sub_msg) -> editor_update(model, sub_msg) + MouseEvent(sub_msg) -> mouse_update(model, sub_msg) + KeyboardMessage(sub_msg) -> keyboard_update(model, sub_msg) + } +} + +fn editor_update(model: Model, msg: EditorMsg) -> #(Model, Effect(Msg)) { + case msg { + EditStarted -> #(Model(..model, editing: True), effect.none()) + EditStateChanged(_) -> todo + EditFinished -> todo + } +} + +fn api_update(model: Model, msg: ApiMsg) -> #(Model, Effect(Msg)) { + case msg { + CellValueCommitted(response) -> { + case response { + Ok(_) -> #(Model(..model, editing: False), blur_hoverbar()) + Error(rsvp.HttpError(response)) -> { + io.println_error("HTTP error while updating value: " <> response.body) + #(model, effect.none()) + } + Error(err) -> { + echo err + #(model, effect.none()) + } + } + } + } +} + +fn mouse_update(model: Model, msg: ApiMsg) -> #(Model, Effect(Msg)) { + case msg { + UserClickedCell(row, column) -> assign_selection(model:, to: #(row, column)) + // TODO + UserDoubleClickedCell(_row, _column) -> #(model, case model.editing { + True -> effect.none() + False -> focus_hoverbar() + }) + } +} + +fn keyboard_update(model: Model, msg: KeyboardMsg) -> #(Model, Effect(Msg)) { + case msg { + UserPressedDirectionalKey(key) -> { + case model.editing, model.selections { + False, [#(selected_row, selected_col)] -> { + let first_row_selected = selected_row == 0 + // No need to subtract 1 from pkeys size, because there's another + // row for "insert". + let last_row_selected = selected_row == dict.size(model.pkeys) + let first_col_selected = selected_col == 0 + let last_col_selected = selected_col == dict.size(model.fields) - 1 + case + key, + first_row_selected, + last_row_selected, + first_col_selected, + last_col_selected + { + "ArrowLeft", _, _, False, _ -> + assign_selection(model:, to: #(selected_row, selected_col - 1)) + "ArrowRight", _, _, _, False -> + assign_selection(model:, to: #(selected_row, selected_col + 1)) + "ArrowUp", False, _, _, _ -> + assign_selection(model:, to: #(selected_row - 1, selected_col)) + "ArrowDown", _, False, _, _ -> + assign_selection(model:, to: #(selected_row + 1, selected_col)) + _, _, _, _, _ -> #(model, effect.none()) + } + } + False, [] -> + case dict.size(model.pkeys) > 0 && dict.size(model.fields) > 0 { + True -> assign_selection(model:, to: #(0, 0)) + False -> #(model, effect.none()) + } + _, _ -> #(model, effect.none()) + } + } + UserPressedEnterKey -> #(model, case model.editing { + True -> effect.none() + False -> focus_hoverbar() + }) + UserPressedTypingKey(key) -> + case model.editing, model.selections, set.is_empty(model.modifiers_held) { + False, [_], True -> #(model, overwrite_in_hoverbar(value: key)) + _, _, _ -> #(model, effect.none()) + } + UserPressedModifierKey(key) -> #( + Model(..model, modifiers_held: model.modifiers_held |> set.insert(key)), + effect.none(), + ) + UserReleasedModifierKey(key) -> #( + Model(..model, modifiers_held: model.modifiers_held |> set.delete(key)), + effect.none(), + ) + } +} + +fn overwrite_in_hoverbar(value value: String) -> Effect(Msg) { + use _, _ <- effect.before_paint() + do_overwrite_in_hoverbar(value) +} + +@external(javascript, "./viewer_controller_component.ffi.mjs", "overwriteInHoverbar") +fn do_overwrite_in_hoverbar(value _value: String) -> Nil { + Nil +} + +fn commit_change(model model: Model, value value: Encodable) -> Effect(Msg) { + case model.selections { + [#(row, col)] -> + rsvp.post( + "./update-value", + json.object([ + #( + "column", + model.fields + |> dict.get(col) + |> result.map(fn(field) { field.name }) + |> result.unwrap("") + |> json.string(), + ), + #( + "pkeys", + model.pkeys + |> dict.get(row) + |> result.unwrap(dict.new()) + |> json.dict(fn(x) { x }, encodable.to_json), + ), + #("value", encodable.to_json(value)), + ]), + rsvp.expect_ok_response(ServerUpdateValuePost), + ) + _ -> todo + } +} + +/// Updater for when selection is being assigned to a single cell. +fn assign_selection( + model model: Model, + to coords: #(Int, Int), +) -> #(Model, Effect(Msg)) { + // Multiple sets of conditions fall back to the "selection actually changed" + // scenario, so this is a little awkward to write without a return keyword. + let early_return = case model.selections { + [prev_coords] -> + case prev_coords == coords { + True -> Some(#(model, effect.none())) + False -> None + } + _ -> None + } + case early_return { + Some(value) -> value + None -> #( + Model(..model, selections: [coords]), + effect.batch([ + sync_cell_value_to_hoverbar( + coords, + model.fields + |> dict.get(pair.second(coords)) + |> result.map(fn(f) { f.field_type }) + |> result.unwrap(field_type.Unknown), + ), + change_selected_attrs([coords]), + ]), + ) + } +} + +// -------- Effects -------- // + +fn focus_hoverbar() -> Effect(Msg) { + use _dispatch, _root <- effect.before_paint() + do_focus_hoverbar() +} + +@external(javascript, "./viewer_controller_component.ffi.mjs", "focusHoverbar") +fn do_focus_hoverbar() -> Nil { + Nil +} + +fn blur_hoverbar() -> Effect(Msg) { + use _dispatch, _root <- effect.before_paint() + do_blur_hoverbar() +} + +@external(javascript, "./viewer_controller_component.ffi.mjs", "blurHoverbar") +fn do_blur_hoverbar() -> Nil { + Nil +} + +/// Effect that changes [selected=] attributes on assigned children. Should only +/// be called when the new selection is different than the previous one (that +/// is, not when selection is "moving" from a cell to the same cell). +fn change_selected_attrs(to_coords coords: List(#(Int, Int))) -> Effect(msg) { + use _, _ <- effect.before_paint() + do_clear_selected_attrs() + coords + |> list.map(fn(coords) { + let #(row, column) = coords + do_set_cell_attr(row:, column:, key: "selected", value: "true") + }) + Nil +} + +fn set_cell_value(coords coords: #(Int, Int), value value: Encodable) { + use _, _ <- effect.before_paint() + let #(row, col) = coords + do_set_cell_attr( + row, + col, + "value", + value |> encodable.to_json() |> json.to_string(), + ) +} + +@external(javascript, "./viewer_controller_component.ffi.mjs", "clearSelectedAttrs") +fn do_clear_selected_attrs() -> Nil { + Nil +} + +@external(javascript, "./viewer_controller_component.ffi.mjs", "setCellAttr") +fn do_set_cell_attr( + row _row: Int, + column _column: Int, + key _key: String, + value _value: String, +) -> Nil { + Nil +} + +fn sync_cell_value_to_hoverbar( + coords coords: #(Int, Int), + field_type f_type: FieldType, +) -> Effect(Msg) { + use _, _root <- effect.before_paint() + let #(row, column) = coords + do_sync_cell_value_to_hoverbar( + row:, + column:, + field_type: f_type |> field_type.to_json() |> json.to_string(), + ) +} + +@external(javascript, "./viewer_controller_component.ffi.mjs", "syncCellValueToHoverbar") +fn do_sync_cell_value_to_hoverbar( + row _row: Int, + column _column: Int, + field_type _field_type: String, +) -> Nil { + Nil +} + +// -------- View -------- // + +fn view(_: Model) -> Element(Msg) { + html.div( + [ + context.on_context_request( + dynamic.string("root_path"), + ChildRequestedRootPath, + ), + event.on("cell-click", { + use msg <- decode.field("detail", { + use row <- decode.field("row", decode.int) + use column <- decode.field("column", decode.int) + decode.success(UserClickedCell(row, column)) + }) + decode.success(msg) + }), + event.on("cell-dblclick", { + use msg <- decode.field("detail", { + use row <- decode.field("row", decode.int) + use column <- decode.field("column", decode.int) + decode.success(UserDoubleClickedCell(row, column)) + }) + decode.success(msg) + }), + event.on("edit-start", decode.success(ChildEmittedEditStartEvent)), + event.on("edit-update", { + use original <- decode.subfield( + ["detail", "original"], + encodable.decoder(), + ) + use edited <- decode.subfield(["detail", "edited"], encodable.decoder()) + decode.success(ChildEmittedEditUpdateEvent(original:, edited:)) + }), + event.on("edit-end", { + use original <- decode.subfield( + ["detail", "original"], + encodable.decoder(), + ) + use edited <- decode.subfield(["detail", "edited"], encodable.decoder()) + decode.success(ChildEmittedEditEndEvent(original:, edited:)) + }), + ], + [component.default_slot([], [])], + ) +} diff --git a/webc/src/viewer_controller_component.gleam b/webc/src/viewer_controller_component.gleam index afd2449..c1ec0ae 100644 --- a/webc/src/viewer_controller_component.gleam +++ b/webc/src/viewer_controller_component.gleam @@ -160,10 +160,17 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { _ -> todo } } - ChildEmittedEditEndEvent(_original, edited) -> #( - model, - commit_change(model:, value: edited), - ) + ChildEmittedEditEndEvent(_original, edited) -> + case model.selections { + [#(selected_row, _)] -> + case selected_row == dict.size(model.pkeys) { + // Cell under edit is in "insert" row + True -> #(Model(..model, editing: False), effect.none()) + False -> #(model, commit_change(model:, value: edited)) + } + _ -> todo + } + ChildRequestedRootPath(callback, subscribe) -> #( case subscribe { True -> @@ -198,7 +205,9 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { case model.editing, model.selections { False, [#(selected_row, selected_col)] -> { let first_row_selected = selected_row == 0 - let last_row_selected = selected_row == dict.size(model.pkeys) - 1 + // No need to subtract 1 from pkeys size, because there's another + // row for "insert". + let last_row_selected = selected_row == dict.size(model.pkeys) let first_col_selected = selected_col == 0 let last_col_selected = selected_col == dict.size(model.fields) - 1 case