diff --git a/.gitignore b/.gitignore index 762de3c..ba5cf2d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ target .env .DS_Store node_modules +css_dist +glm_dist js_dist pgdata .vite diff --git a/components/src/add-selection-button.ts b/components/src/add-selection-button.ts new file mode 100644 index 0000000..f3f542d --- /dev/null +++ b/components/src/add-selection-button.ts @@ -0,0 +1,132 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { createRef, ref } from "lit/directives/ref.js"; + +import "./add-selection-modal-contents.tsx"; + +@customElement("add-selection-button") +export class AddSelectionButton extends LitElement { + @property({ attribute: true }) + columns = ""; + + @state() + private _active = false; + + private _labelInputRef = createRef(); + + private _nameInputRef = createRef(); + + private _typePopoverRef = createRef(); + + static override styles = css` + :host { + height: 100%; + --shadow: 0 0.5rem 0.5rem #3333; + } + + button.main { + appearance: none; + border: none; + width: 100%; + height: 100%; + font-weight: inherit; + font-size: inherit; + font-family: inherit; + cursor: pointer; + background: none; + } + + div.th { + height: 100%; + display: flex; + border: dashed 1px #ccc; + border-left: none; + border-top: none; + font-family: "Funnel Sans"; + background: #0001; + box-sizing: border-box; + padding: 0 0.5rem; + } + + input#name-input { + appearance: none; + background: none; + border: none; + outline: none; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + } + + button#type-config-button { + anchor-name: --type-config-button; + } + + .config-popover:popover-open { + box-sizing: border-box; + inset: unset; + margin: 0; + margin-top: 1rem; + position: fixed; + display: block; + width: 20rem; + border: solid 1px #ccc; + border-radius: 0.25rem; + filter: drop-shadow(var(--shadow)); + } + + #type-popover { + position-anchor: --type-config-button; + position-area: bottom; + } + `; + + protected override updated() { + if (this._active && this._labelInputRef.value) { + this._labelInputRef.value.focus(); + } + } + + private _activate() { + this._active = true; + } + + private _handleLabelChange(ev: InputEvent) { + } + + private _showTypePopover() { + this._typePopoverRef.value?.showPopover(); + } + + protected override render() { + if (this._active) { + return html` +
+ + + +
+
+ +
+ `; + } + + return html` + + `; + } +} diff --git a/components/src/add-selection-button.tsx b/components/src/add-selection-button.tsx deleted file mode 100644 index 89a7ce2..0000000 --- a/components/src/add-selection-button.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { css, html, LitElement } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { createRef, type Ref, ref } from "lit/directives/ref.js"; - -import "./add-selection-modal-contents.tsx"; - -@customElement("add-selection-button") -export class AddSelectionButton extends LitElement { - @property({ attribute: true }) - columns = ""; - - private _dialogRef: Ref = createRef(); - - static override styles = css` - button.th { - appearance: none; - border: none; - width: 100%; - height: 100%; - font-weight: inherit; - font-size: inherit; - font-family: inherit; - cursor: pointer; - background: none; - } - - dialog { - border: solid 1px #ccc; - border-radius: 0.5rem; - box-shadow: 0 0.5rem 0.5rem #3333; - font-family: - "Averia Serif Libre", - "Open Sans", - "Helvetica Neue", - Arial, - sans-serif; - } - - dialog::backdrop { - background: #0001; - } - `; - - showModal() { - this._dialogRef.value?.showModal(); - } - - protected override render() { - return html` - - - - - `; - } -} diff --git a/components/src/entrypoints/viewer-components.ts b/components/src/entrypoints/viewer-components.ts new file mode 100644 index 0000000..82acd3d --- /dev/null +++ b/components/src/entrypoints/viewer-components.ts @@ -0,0 +1 @@ +export { AddSelectionButton } from "../add-selection-button.ts"; diff --git a/components/src/entrypoints/viewer-components.tsx b/components/src/entrypoints/viewer-components.tsx deleted file mode 100644 index 5b2e7e3..0000000 --- a/components/src/entrypoints/viewer-components.tsx +++ /dev/null @@ -1 +0,0 @@ -export { AddSelectionButton } from "../add-selection-button.tsx"; diff --git a/glm/.gitignore b/glm/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/glm/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/glm/README.md b/glm/README.md new file mode 100644 index 0000000..fbe688a --- /dev/null +++ b/glm/README.md @@ -0,0 +1,24 @@ +# glm + +[![Package Version](https://img.shields.io/hexpm/v/glm)](https://hex.pm/packages/glm) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/glm/) + +```sh +gleam add glm@1 +``` +```gleam +import glm + +pub fn main() -> Nil { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/glm/gleam.toml b/glm/gleam.toml new file mode 100644 index 0000000..c25b6f9 --- /dev/null +++ b/glm/gleam.toml @@ -0,0 +1,24 @@ +name = "glm" +version = "1.0.0" +target = "javascript" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" +lustre = ">= 5.2.1 and < 6.0.0" +gleam_json = ">= 3.0.2 and < 4.0.0" +gleam_regexp = ">= 1.1.1 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" +lustre_dev_tools = ">= 1.9.0 and < 2.0.0" diff --git a/glm/manifest.toml b/glm/manifest.toml new file mode 100644 index 0000000..415b6a2 --- /dev/null +++ b/glm/manifest.toml @@ -0,0 +1,51 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, + { name = "directories", version = "1.2.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_stdlib", "platform", "simplifile"], otp_app = "directories", source = "hex", outer_checksum = "D13090CFCDF6759B87217E8DDD73A75903A700148A82C1D33799F333E249BF9E" }, + { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, + { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, + { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, + { name = "fs", version = "11.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "DD00A61D89EAC01D16D3FC51D5B0EB5F0722EF8E3C1A3A547CD086957F3260A9" }, + { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, + { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, + { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, + { name = "gleam_deque", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_deque", source = "hex", outer_checksum = "64D77068931338CF0D0CB5D37522C3E3CCA7CB7D6C5BACB41648B519CC0133C7" }, + { name = "gleam_erlang", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "F91CE62A2D011FA13341F3723DB7DB118541AAA5FE7311BD2716D018F01EF9E3" }, + { name = "gleam_http", version = "4.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "DB25DFC8530B64B77105405B80686541A0D96F7E2D83D807D6B2155FB9A8B1B8" }, + { name = "gleam_httpc", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C670EBD46FC1472AD5F1F74F1D3938D1D0AC1C7531895ED1D4DDCB6F07279F43" }, + { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" }, + { name = "gleam_otp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7020E652D18F9ABAC9C877270B14160519FA0856EE80126231C505D719AD68DA" }, + { name = "gleam_package_interface", version = "3.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "8F2D19DE9876D9401BB0626260958A6B1580BB233489C32831FE74CE0ACAE8B4" }, + { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, + { name = "gleam_stdlib", version = "0.62.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "DC8872BC0B8550F6E22F0F698CFE7F1E4BDA7312FDEB40D6C3F44C5B706C8310" }, + { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, + { name = "gleeunit", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "63022D81C12C17B7F1A60E029964E830A4CBD846BBC6740004FC1F1031AE0326" }, + { name = "glint", version = "1.2.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "2214C7CEFDE457CEE62140C3D4899B964E05236DA74E4243DFADF4AF29C382BB" }, + { name = "glisten", version = "8.0.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "534BB27C71FB9E506345A767C0D76B17A9E9199934340C975DC003C710E3692D" }, + { name = "gramps", version = "3.0.3", build_tools = ["gleam"], requirements = ["gleam_crypto", "gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gramps", source = "hex", outer_checksum = "75F0F20C867A6217CBB632A7E563568D6A6366B850815041E8E0B4F179681E53" }, + { name = "houdini", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "houdini", source = "hex", outer_checksum = "5BA517E5179F132F0471CB314F27FE210A10407387DA1EA4F6FD084F74469FC2" }, + { name = "hpack_erl", version = "0.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "hpack", source = "hex", outer_checksum = "D6137D7079169D8C485C6962DFE261AF5B9EF60FBC557344511C1E65E3D95FB0" }, + { name = "logging", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "logging", source = "hex", outer_checksum = "1098FBF10B54B44C2C7FDF0B01C1253CAFACDACABEFB4B0D027803246753E06D" }, + { name = "lustre", version = "5.2.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_json", "gleam_otp", "gleam_stdlib", "houdini"], otp_app = "lustre", source = "hex", outer_checksum = "DCD121F8E6B7E179B27D9A8AEB6C828D8380E26DF2E16D078511EDAD1CA9F2A7" }, + { name = "lustre_dev_tools", version = "1.9.0", build_tools = ["gleam"], requirements = ["argv", "filepath", "fs", "gleam_community_ansi", "gleam_crypto", "gleam_deque", "gleam_erlang", "gleam_http", "gleam_httpc", "gleam_json", "gleam_otp", "gleam_package_interface", "gleam_regexp", "gleam_stdlib", "glint", "glisten", "mist", "repeatedly", "simplifile", "term_size", "tom", "wisp"], otp_app = "lustre_dev_tools", source = "hex", outer_checksum = "2132E6B2B7E89ED87C138FFE1F2CD70D859258D67222F26B5793CDACE9B07D75" }, + { name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" }, + { name = "mist", version = "5.0.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "0716CE491EA13E1AA1EFEC4B427593F8EB2B953B6EBDEBE41F15BE3D06A22918" }, + { name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" }, + { name = "repeatedly", version = "2.1.2", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "93AE1938DDE0DC0F7034F32C1BF0D4E89ACEBA82198A1FE21F604E849DA5F589" }, + { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" }, + { name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" }, + { name = "telemetry", version = "1.3.0", build_tools = ["rebar3"], requirements = [], otp_app = "telemetry", source = "hex", outer_checksum = "7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6" }, + { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" }, + { name = "tom", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0910EE688A713994515ACAF1F486A4F05752E585B9E3209D8F35A85B234C2719" }, + { name = "wisp", version = "1.8.0", build_tools = ["gleam"], requirements = ["directories", "exception", "gleam_crypto", "gleam_erlang", "gleam_http", "gleam_json", "gleam_stdlib", "houdini", "logging", "marceau", "mist", "simplifile"], otp_app = "wisp", source = "hex", outer_checksum = "0FE9049AFFB7C8D5FC0B154EEE2704806F4D51B97F44925D69349B3F4F192957" }, +] + +[requirements] +gleam_json = { version = ">= 3.0.2 and < 4.0.0" } +gleam_regexp = { version = ">= 1.1.1 and < 2.0.0" } +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +lustre = { version = ">= 5.2.1 and < 6.0.0" } +lustre_dev_tools = { version = ">= 1.9.0 and < 2.0.0" } diff --git a/glm/src/field_adder.ffi.mjs b/glm/src/field_adder.ffi.mjs new file mode 100644 index 0000000..b7b9327 --- /dev/null +++ b/glm/src/field_adder.ffi.mjs @@ -0,0 +1,10 @@ +import { Error, Ok } from "./gleam.mjs"; + +export function focusElement(selector, root) { + const element = root.querySelector(selector); + if (element) { + element.focus(); + return new Ok(undefined); + } + return new Error(undefined); +} diff --git a/glm/src/field_adder.gleam b/glm/src/field_adder.gleam new file mode 100644 index 0000000..92b0f84 --- /dev/null +++ b/glm/src/field_adder.gleam @@ -0,0 +1,239 @@ +import gleam/dynamic.{type Dynamic} +import gleam/dynamic/decode +import gleam/json +import gleam/regexp +import gleam/result +import gleam/string +import lustre.{type App} +import lustre/attribute as attr +import lustre/component +import lustre/effect.{type Effect} +import lustre/element.{type Element} +import lustre/element/html +import lustre/event + +pub const name: String = "field-adder" + +pub fn component() -> App(Nil, Model, Msg) { + lustre.component(init, update, view, [ + component.on_attribute_change("columns", fn(value) { + Ok( + json.parse(from: value, using: decode.list(of: decode.string)) + |> result.unwrap([]) + |> ParentChangedColumns, + ) + }), + component.on_attribute_change("root-path", fn(value) { + ParentChangedRootPath(value) |> Ok + }), + ]) +} + +pub type Model { + Model( + columns: List(String), + root_path: String, + expanded: Bool, + label_value: String, + name_value: String, + name_customized: Bool, + field_type: String, + submitting: Bool, + ) +} + +fn init(_) -> #(Model, Effect(Msg)) { + #( + Model( + columns: [], + root_path: "", + expanded: False, + label_value: "", + name_value: "", + name_customized: False, + field_type: "text", + submitting: False, + ), + effect.none(), + ) +} + +pub type Msg { + ParentChangedColumns(List(String)) + ParentChangedRootPath(String) + UserClickedCancel + UserExpandedComponent + UserUpdatedName(String) + UserUpdatedLabel(String) + UserUpdatedFieldType(String) +} + +fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { + case msg { + ParentChangedColumns(columns) -> #(Model(..model, columns:), effect.none()) + ParentChangedRootPath(root_path) -> #( + Model(..model, root_path:), + effect.none(), + ) + UserClickedCancel -> #( + Model( + ..model, + expanded: False, + label_value: "", + name_value: "", + name_customized: False, + field_type: "text", + ), + effect.none(), + ) + UserExpandedComponent -> #( + Model(..model, expanded: True), + focus_element("#label-input"), + ) + UserUpdatedName(name_value) -> #( + Model(..model, name_value:, name_customized: True), + effect.none(), + ) + UserUpdatedLabel(label_value) -> #( + Model(..model, label_value:, name_value: case model.name_customized { + True -> model.name_value + False -> label_value |> to_idiomatic_column_name + }), + effect.none(), + ) + UserUpdatedFieldType(field_type) -> #( + Model(..model, field_type:), + effect.none(), + ) + } +} + +fn to_idiomatic_column_name(label: String) -> String { + let assert Ok(re) = regexp.from_string("[^a-z0-9]") + regexp.replace(each: re, in: label |> string.lowercase, with: "_") +} + +fn focus_element(selector: String) -> Effect(Msg) { + use _, shadow_root <- effect.before_paint + do_focus_element(selector:, in: shadow_root) |> result.unwrap(Nil) +} + +@external(javascript, "./field_adder.ffi.mjs", "focusElement") +fn do_focus_element( + selector _selector: String, + in _root: Dynamic, +) -> Result(Nil, Nil) { + Error(Nil) +} + +fn view(model: Model) -> Element(Msg) { + element.fragment([ + html.link([ + attr.rel("stylesheet"), + attr.href(model.root_path <> "/css_dist/field_adder/index.css"), + ]), + case model.expanded { + False -> + html.button( + [ + attr.type_("button"), + attr.class("expander__button"), + event.on_click(UserExpandedComponent), + ], + [html.text("+")], + ) + True -> + html.div([attr.class("header")], [ + html.form([attr.method("post"), attr.action("create-column")], [ + label_input(value: model.label_value, on_input: UserUpdatedLabel), + html.button( + [attr.type_("button"), attr.popovertarget("config-popover")], + [html.text("...")], + ), + config_popover(model), + ]), + ]) + }, + ]) +} + +fn label_input( + value value: String, + on_input handle_input: fn(String) -> Msg, +) -> Element(Msg) { + html.input([ + attr.type_("text"), + attr.id("label-input"), + attr.name("label"), + attr.class("header__input"), + attr.placeholder("My New Column"), + attr.value(value), + event.on_input(handle_input), + ]) +} + +fn config_popover(model: Model) -> Element(Msg) { + html.div( + [ + attr.id("config-popover"), + attr.class("config-popover__container"), + attr.popover("auto"), + ], + [ + html.h2([attr.class("form-section__heading")], [ + html.text("Field Details"), + ]), + html.label([attr.class("form-section__label"), attr.for("name-input")], [ + html.text("SQL-friendly Name"), + ]), + html.input([ + attr.type_("text"), + attr.name("name"), + attr.class("form-section__input form-section__input--text"), + attr.id("name-input"), + attr.value(model.name_value), + event.on_input(UserUpdatedName), + ]), + html.label( + [attr.for("field-type-select"), attr.class("form-section__label")], + [html.text("Data Type")], + ), + html.select( + [ + attr.type_("text"), + attr.name("field_type"), + attr.class("form-section__input"), + attr.id("field-type-select"), + event.on_change(UserUpdatedFieldType), + ], + [ + html.option( + [attr.value("text"), attr.checked(model.field_type == "text")], + "Text", + ), + html.option( + [attr.value("decimal"), attr.checked(model.field_type == "decimal")], + "Decimal", + ), + ], + ), + html.div([attr.class("form-buttons")], [ + html.button( + [ + attr.type_("button"), + attr.class("form-buttons__button form-buttons__button--cancel"), + event.on_click(UserClickedCancel), + ], + [html.text("Cancel")], + ), + html.button( + [ + attr.type_("submit"), + attr.class("form-buttons__button form-buttons__button--submit"), + ], + [html.text("Create")], + ), + ]), + ], + ) +} diff --git a/glm/src/glm.gleam.bak b/glm/src/glm.gleam.bak new file mode 100644 index 0000000..e7a0ddc --- /dev/null +++ b/glm/src/glm.gleam.bak @@ -0,0 +1,6 @@ +import field_adder + +pub fn main() -> Nil { + let assert Ok(_) = field_adder.register() + Nil +} diff --git a/interim-server/src/router.rs b/interim-server/src/router.rs index 19cdc97..3c83a4c 100644 --- a/interim-server/src/router.rs +++ b/interim-server/src/router.rs @@ -107,6 +107,46 @@ pub fn new_router(state: AppState) -> Router<()> { ), ), ) + .nest_service( + "/glm_dist", + ServiceBuilder::new() + .layer(SetResponseHeaderLayer::if_not_present( + CACHE_CONTROL, + // FIXME: restore production value + // HeaderValue::from_static("max-age=21600, stale-while-revalidate=86400"), + HeaderValue::from_static("no-cache"), + )) + .service( + ServeDir::new("glm_dist").not_found_service( + ServiceBuilder::new() + .layer(SetResponseHeaderLayer::if_not_present( + CACHE_CONTROL, + HeaderValue::from_static("no-cache"), + )) + .service(ServeFile::new("static/_404.html")), + ), + ), + ) + .nest_service( + "/css_dist", + ServiceBuilder::new() + .layer(SetResponseHeaderLayer::if_not_present( + CACHE_CONTROL, + // FIXME: restore production value + // HeaderValue::from_static("max-age=21600, stale-while-revalidate=86400"), + HeaderValue::from_static("no-cache"), + )) + .service( + ServeDir::new("css_dist").not_found_service( + ServiceBuilder::new() + .layer(SetResponseHeaderLayer::if_not_present( + CACHE_CONTROL, + HeaderValue::from_static("no-cache"), + )) + .service(ServeFile::new("static/_404.html")), + ), + ), + ) .fallback_service( ServiceBuilder::new() .layer(SetResponseHeaderLayer::if_not_present( diff --git a/interim-server/templates/base.html b/interim-server/templates/base.html index 0e02f84..545c588 100644 --- a/interim-server/templates/base.html +++ b/interim-server/templates/base.html @@ -5,6 +5,7 @@ {% include "meta_tags.html" %} + {% block main %}{% endblock main %} diff --git a/interim-server/templates/lens.html b/interim-server/templates/lens.html index 68692f6..eb7a001 100644 --- a/interim-server/templates/lens.html +++ b/interim-server/templates/lens.html @@ -5,6 +5,7 @@ + @@ -14,8 +15,8 @@
{{ field.label.clone().unwrap_or(field.name.clone()) }}
{% endfor %} - diff --git a/mise.toml b/mise.toml index b96692f..5beb385 100644 --- a/mise.toml +++ b/mise.toml @@ -1,8 +1,12 @@ [tools] deno = "latest" +erlang = "latest" +gleam = "latest" jujutsu = "latest" +rebar = "latest" 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" @@ -17,6 +21,15 @@ run = "deno task build" dir = "./components" sources = ["**/*.ts"] +[tasks.lustre] +run = "gleam run -m lustre/dev build component field_adder --outdir=../glm_dist" +dir = "./glm" +sources = ["**/*.gleam"] + +[tasks.sass] +run = "sass sass/:css_dist/" +sources = ["sass/**/*.scss"] + [env] RUST_LOG = "debug" RUST_BACKTRACE = "1" diff --git a/sass/_forms.scss b/sass/_forms.scss new file mode 100644 index 0000000..fdc8c6c --- /dev/null +++ b/sass/_forms.scss @@ -0,0 +1,57 @@ +@use 'globals'; + +$section-gap: 1.5rem; +$label-gap: 0.5rem; +$button-gap: 0.25rem; + +.form-section { + &__heading { + margin: 0; + font-size: 1rem; + font-weight: 700; + } + + &__label { + display: block; + font-weight: 600; + margin-top: $section-gap; + } + + &__input { + display: block; + margin-top: $label-gap; + font-family: globals.$font-family-data; + + &--text { + @include globals.rounded; + border: globals.$default-border; + padding: 0.5rem; + } + } +} + +.form-buttons { + display: flex; + margin-top: $section-gap; + justify-content: flex-end; + + &__button { + margin: 0 $button-gap; + + &:first-child { + margin-left: 0; + } + + &:last-child { + margin-right: 0; + } + + &--cancel { + @include globals.button-clear; + } + + &--submit { + @include globals.button-primary; + } + } +} diff --git a/sass/_globals.scss b/sass/_globals.scss new file mode 100644 index 0000000..29b5f1d --- /dev/null +++ b/sass/_globals.scss @@ -0,0 +1,54 @@ +@use 'sass:color'; + +$button-primary-background: #07f; +$button-primary-color: #fff; +$default-border: solid 1px #ccc; +$font-family-default: 'Averia Serif Libre', 'Open Sans', 'Helvetica Neue', Arial, sans-serif; +$font-family-data: 'Funnel Sans', 'Open Sans', 'Helvetica Neue', Arial, sans-serif; +$popover-border: $default-border; +$popover-shadow: 0 0.5rem 0.5rem #3333; + +@mixin reset-button { + appearance: none; + background: none; + border: none; + box-sizing: border-box; + cursor: pointer; + font-family: inherit; + font-size: inherit; + font-weight: inherit; +} + +@mixin button-base { + @include reset-button; + @include rounded; + font-family: $font-family-default; + padding: 0.5rem 1rem; + transition: background 0.2s ease; +} + +@mixin button-primary { + @include button-base; + background: $button-primary-background; + color: $button-primary-color; + + &:hover { + background: color.scale($button-primary-background, $lightness: -10%, $space: oklch); + } +} + +@mixin button-clear { + @include button-base; + + &:hover { + background: #0002; + } +} + +@mixin rounded-sm { + border-radius: 0.25rem; +} + +@mixin rounded { + border-radius: 0.5rem; +} diff --git a/sass/_viewer-shared.scss b/sass/_viewer-shared.scss new file mode 100644 index 0000000..52fdba9 --- /dev/null +++ b/sass/_viewer-shared.scss @@ -0,0 +1,16 @@ +@use 'globals'; + +@mixin th { + border: globals.$default-border; + border-top: none; + font-family: 'Funnel Sans'; + font-weight: bolder; + background: #0001; + height: 100%; /* css hack to make percentage based cell heights work */ + padding: 0.25rem 0.5rem; + text-align: left; + + &:first-child { + border-left: none; + } +} diff --git a/sass/field_adder/_styles.scss b/sass/field_adder/_styles.scss new file mode 100644 index 0000000..3a37cd6 --- /dev/null +++ b/sass/field_adder/_styles.scss @@ -0,0 +1,48 @@ +@use '../globals'; +@use '../viewer-shared'; + +field-adder { + height: 100%; + + &::part(expander) { + @include globals.reset-button; + width: 100%; + height: 100%; + } + + &::part(th-lookalike) { + @include viewer-shared.th; + border-right-style: dashed; + border-bottom-style: dashed; + border-left: none; + display: flex; + } + + &::part(label-input) { + appearance: none; + background: none; + border: none; + outline: none; + font-weight: inherit; + } + + &::part(config-popover) { + @include globals.rounded-sm; + position: fixed; + inset: unset; + margin: 0; + width: 20rem; + padding: 1rem; + font-weight: normal; + border: globals.$popover-border; + filter: drop-shadow(globals.$popover-shadow); + + &:popover-open { + display: block; + } + } + + &::part(form-section-header) { + margin: 0; + } +} diff --git a/sass/field_adder/index.scss b/sass/field_adder/index.scss new file mode 100644 index 0000000..0f39907 --- /dev/null +++ b/sass/field_adder/index.scss @@ -0,0 +1,48 @@ +@use '../globals'; +@use '../forms'; +@use '../viewer-shared'; + +:host { + height: 100%; +} + +.expander__button { + @include globals.reset-button; + width: 100%; + height: 100%; +} + +.header { + @include viewer-shared.th; + border-right-style: dashed; + border-bottom-style: dashed; + border-left: none; + display: flex; + + &__input { + appearance: none; + background: none; + border: none; + outline: none; + font-weight: inherit; + } +} + +.config-popover { + &__container { + @include globals.rounded; + font-family: globals.$font-family-default; + position: fixed; + inset: unset; + margin: 0; + width: 20rem; + padding: 1rem; + font-weight: normal; + border: globals.$popover-border; + filter: drop-shadow(globals.$popover-shadow); + + &:popover-open { + display: block; + } + } +} diff --git a/sass/main.scss b/sass/main.scss new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/sass/main.scss @@ -0,0 +1 @@ + diff --git a/static/averia_serif_libre/AveriaSerifLibre-Bold.ttf b/static/averia_serif_libre/averia_serif_libre_bold.ttf similarity index 100% rename from static/averia_serif_libre/AveriaSerifLibre-Bold.ttf rename to static/averia_serif_libre/averia_serif_libre_bold.ttf diff --git a/static/averia_serif_libre/AveriaSerifLibre-Light.ttf b/static/averia_serif_libre/averia_serif_libre_light.ttf similarity index 100% rename from static/averia_serif_libre/AveriaSerifLibre-Light.ttf rename to static/averia_serif_libre/averia_serif_libre_light.ttf diff --git a/static/css/field_adder.css b/static/css/field_adder.css new file mode 100644 index 0000000..b6020c7 --- /dev/null +++ b/static/css/field_adder.css @@ -0,0 +1,18 @@ +field-adder { + --popover-border: solid 1px #ccc; + --popover-shadow: 0 0.5rem 0.5rem #3333; + + height: 100%; + + & button.expander { + appearance: none; + border: none; + width: 100%; + height: 100%; + font-weight: inherit; + font-size: inherit; + font-family: inherit; + cursor: pointer; + background: none; + } +} diff --git a/static/main.css b/static/main.css index 818f479..9069802 100644 --- a/static/main.css +++ b/static/main.css @@ -11,6 +11,18 @@ button, input[type="submit"] { src: url("./averia_serif_libre/averia_serif_libre_regular.ttf"); } +@font-face { + font-family: "Averia Serif Libre"; + src: url("./averia_serif_libre/averia_serif_libre_bold.ttf"); + font-weight: 700; +} + +@font-face { + font-family: "Averia Serif Libre"; + src: url("./averia_serif_libre/averia_serif_libre_light.ttf"); + font-weight: 300; +} + @font-face { font-family: "Funnel Sans"; src: url("./funnel_sans/funnel_sans_variable.ttf"); diff --git a/static/viewer.css b/static/viewer.css index b696d5d..28e37d3 100644 --- a/static/viewer.css +++ b/static/viewer.css @@ -1,5 +1,6 @@ table.viewer { border-collapse: collapse; + height: 1px; /* css hack to make percentage based cell heights work */ } table.viewer > thead > tr > th { @@ -7,10 +8,19 @@ table.viewer > thead > tr > th { border-top: none; font-family: "Funnel Sans"; background: #0001; + height: 100%; /* css hack to make percentage based cell heights work */ + padding: 0 0.5rem; + text-align: left; &:first-child { border-left: none; } + + &.column-adder { + border: none; + background: none; + padding: 0; + } } table.viewer .padded-cell {
- + +