set up lustre and sass

This commit is contained in:
Brent Schroeter 2025-07-16 22:03:04 -07:00
parent 9c1c11a277
commit afafb49cd6
27 changed files with 814 additions and 61 deletions

2
.gitignore vendored
View file

@ -2,6 +2,8 @@ target
.env .env
.DS_Store .DS_Store
node_modules node_modules
css_dist
glm_dist
js_dist js_dist
pgdata pgdata
.vite .vite

View file

@ -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<HTMLInputElement>();
private _nameInputRef = createRef<HTMLInputElement>();
private _typePopoverRef = createRef<HTMLDivElement>();
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`
<div class="th">
<input
type="text"
${ref(
this._labelInputRef,
)}
id="label-input"
name="name-input"
@change="${this._handleLabelChange}"
>
<button type="button" id="type-config-button" @click="${this
._showTypePopover}">
abc
</button>
<button type="submit">Create</button>
</div>
<div ${ref(
this._typePopoverRef,
)} id="type-popover" class="config-popover" popover="auto">
<input type="text" ${ref(this._nameInputRef)} name="name">
</div>
`;
}
return html`
<button type="button" class="main" @click="${this._activate}">+</button>
`;
}
}

View file

@ -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<HTMLDialogElement> = 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`
<button type="button" class="th" @click="${this.showModal}">+</button>
<dialog ${ref(this._dialogRef)} closedby="any">
<add-selection-modal-content
columns="${this.columns}"
></add-selection-modal-content>
</dialog>
`;
}
}

View file

@ -0,0 +1 @@
export { AddSelectionButton } from "../add-selection-button.ts";

View file

@ -1 +0,0 @@
export { AddSelectionButton } from "../add-selection-button.tsx";

4
glm/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.beam
*.ez
/build
erl_crash.dump

24
glm/README.md Normal file
View file

@ -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 <https://hexdocs.pm/glm>.
## Development
```sh
gleam run # Run the project
gleam test # Run the tests
```

24
glm/gleam.toml Normal file
View file

@ -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"

51
glm/manifest.toml Normal file
View file

@ -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" }

View file

@ -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);
}

239
glm/src/field_adder.gleam Normal file
View file

@ -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")],
),
]),
],
)
}

6
glm/src/glm.gleam.bak Normal file
View file

@ -0,0 +1,6 @@
import field_adder
pub fn main() -> Nil {
let assert Ok(_) = field_adder.register()
Nil
}

View file

@ -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( .fallback_service(
ServiceBuilder::new() ServiceBuilder::new()
.layer(SetResponseHeaderLayer::if_not_present( .layer(SetResponseHeaderLayer::if_not_present(

View file

@ -5,6 +5,7 @@
{% include "meta_tags.html" %} {% include "meta_tags.html" %}
<link rel="stylesheet" href="{{ settings.root_path }}/modern-normalize.min.css"> <link rel="stylesheet" href="{{ settings.root_path }}/modern-normalize.min.css">
<link rel="stylesheet" href="{{ settings.root_path }}/main.css"> <link rel="stylesheet" href="{{ settings.root_path }}/main.css">
<link rel="stylesheet" href="{{ settings.root_path }}/css_dist/main.css">
</head> </head>
<body> <body>
{% block main %}{% endblock main %} {% block main %}{% endblock main %}

View file

@ -5,6 +5,7 @@
<script type="module" src="{{ settings.root_path }}/js_dist/lens-controls.mjs"></script> <script type="module" src="{{ settings.root_path }}/js_dist/lens-controls.mjs"></script>
<script type="module" src="{{ settings.root_path }}/js_dist/viewer-components.mjs"></script> <script type="module" src="{{ settings.root_path }}/js_dist/viewer-components.mjs"></script>
<script type="module" src="{{ settings.root_path }}/js_dist/viewer-controller.mjs"></script> <script type="module" src="{{ settings.root_path }}/js_dist/viewer-controller.mjs"></script>
<script type="module" src="{{ settings.root_path }}/glm_dist/field_adder.mjs"></script>
<link rel="stylesheet" href="{{ settings.root_path }}/viewer.css"> <link rel="stylesheet" href="{{ settings.root_path }}/viewer.css">
<table class="viewer"> <table class="viewer">
<thead> <thead>
@ -14,8 +15,8 @@
<div class="padded-cell">{{ field.label.clone().unwrap_or(field.name.clone()) }}</div> <div class="padded-cell">{{ field.label.clone().unwrap_or(field.name.clone()) }}</div>
</th> </th>
{% endfor %} {% endfor %}
<th> <th class="column-adder">
<add-selection-button columns="{{ all_columns | json }}"></add-selection-button> <field-adder root-path="{{ settings.root_path }}" columns="{{ all_columns | json }}"></field-adder>
</th> </th>
</tr> </tr>
</thead> </thead>

View file

@ -1,8 +1,12 @@
[tools] [tools]
deno = "latest" deno = "latest"
erlang = "latest"
gleam = "latest"
jujutsu = "latest" jujutsu = "latest"
rebar = "latest"
rust = { version = "1.88.0", components = "rust-analyzer,clippy" } rust = { version = "1.88.0", components = "rust-analyzer,clippy" }
watchexec = "latest" watchexec = "latest"
"github:sass/dart-sass" = "1.89.2"
[tasks.postgres] [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" 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" dir = "./components"
sources = ["**/*.ts"] 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] [env]
RUST_LOG = "debug" RUST_LOG = "debug"
RUST_BACKTRACE = "1" RUST_BACKTRACE = "1"

57
sass/_forms.scss Normal file
View file

@ -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;
}
}
}

54
sass/_globals.scss Normal file
View file

@ -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;
}

16
sass/_viewer-shared.scss Normal file
View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

1
sass/main.scss Normal file
View file

@ -0,0 +1 @@

View file

@ -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;
}
}

View file

@ -11,6 +11,18 @@ button, input[type="submit"] {
src: url("./averia_serif_libre/averia_serif_libre_regular.ttf"); 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-face {
font-family: "Funnel Sans"; font-family: "Funnel Sans";
src: url("./funnel_sans/funnel_sans_variable.ttf"); src: url("./funnel_sans/funnel_sans_variable.ttf");

View file

@ -1,5 +1,6 @@
table.viewer { table.viewer {
border-collapse: collapse; border-collapse: collapse;
height: 1px; /* css hack to make percentage based cell heights work */
} }
table.viewer > thead > tr > th { table.viewer > thead > tr > th {
@ -7,10 +8,19 @@ table.viewer > thead > tr > th {
border-top: none; border-top: none;
font-family: "Funnel Sans"; font-family: "Funnel Sans";
background: #0001; background: #0001;
height: 100%; /* css hack to make percentage based cell heights work */
padding: 0 0.5rem;
text-align: left;
&:first-child { &:first-child {
border-left: none; border-left: none;
} }
&.column-adder {
border: none;
background: none;
padding: 0;
}
} }
table.viewer .padded-cell { table.viewer .padded-cell {