Changes
13 changed files (+478/-75)
-
-
@@ -32,6 +32,8 @@ compile.addElmSourceFile(b.path("src/Parameters/Constraints.elm"));compile.addElmSourceFile(b.path("src/Parameters/Form.elm")); compile.addElmSourceFile(b.path("src/Parameters/Key.elm")); compile.addElmSourceFile(b.path("src/Parameters/Parser.elm")); compile.addElmSourceFile(b.path("src/Preferences.elm")); compile.addElmSourceFile(b.path("src/Preferences/App.elm")); compile.addElmSourceFile(b.path("src/Template.elm")); compile.addElmSourceFile(b.path("src/Svg/Path.elm"));
-
-
-
@@ -9,11 +9,11 @@ "direct": {"elm/browser": "1.0.2", "elm/core": "1.0.5", "elm/html": "1.0.1", "elm/json": "1.1.4", "elm/svg": "1.0.1", "elm/url": "1.0.0" }, "indirect": { "elm/json": "1.1.4", "elm/time": "1.0.0", "elm/virtual-dom": "1.0.5" }
-
-
-
@@ -11,10 +11,12 @@ module Main exposing (main)import Browser import Browser.Navigation exposing (Key) import Html exposing (node, p) import Html exposing (hr, node, p) import Html.Attributes exposing (attribute) import Json.Decode import Parameters exposing (Parameters) import Parameters.Form import Preferences.App import Template exposing (template) import Url exposing (Url)
-
@@ -36,7 +38,8 @@ -- FLAGStype alias Flags = () { preferences : Json.Decode.Value }
-
@@ -48,11 +51,12 @@ { url : Url, key : Key , parameters : Parameters , parametersForm : Parameters.Form.Model , preferences : Preferences.App.Model } init : Flags -> Url -> Key -> ( Model, Cmd Msg ) init _ url key = init flags url key = let parameters = Parameters.default
-
@@ -61,6 +65,7 @@ ( { url = url, key = key , parameters = parameters , parametersForm = Parameters.Form.init parameters , preferences = Preferences.App.init flags.preferences } , Cmd.none )
-
@@ -73,6 +78,7 @@type Msg = NoOp | ParametersFormMsg Parameters.Form.Msg | PreferencesMsg Preferences.App.Msg | UrlRequested Browser.UrlRequest | UrlChanged Url
-
@@ -91,6 +97,11 @@ Parameters.Form.update subMsg model.parametersForm|> Tuple.mapFirst (\p -> { model | parametersForm = p }) |> Tuple.mapSecond (Cmd.map ParametersFormMsg) PreferencesMsg subMsg -> Preferences.App.update subMsg model.preferences |> Tuple.mapFirst (\p -> { model | preferences = p }) |> Tuple.mapSecond (Cmd.map PreferencesMsg) UrlRequested (Browser.Internal url) -> ( model, Browser.Navigation.replaceUrl model.key (Url.toString url) )
-
@@ -116,11 +127,16 @@ [ attribute "slot" "preview" ][ template model.parameters [] ] , node "x-panel" [ attribute "slot" "parameters" ] [ Parameters.Form.view model.parametersForm model.parameters [ node "x-parameters" [] |> Html.map ParametersFormMsg ((Parameters.Form.view model.parametersForm model.parameters |> List.map (Html.map ParametersFormMsg) ) ++ hr [] [] :: (Preferences.App.panelItems model.preferences |> List.map (Html.map PreferencesMsg)) ) ] ] ]
-
-
-
@@ -349,74 +349,77 @@ ]type alias GroupProps msg = { title : List (Html.Html msg) } { title : List (Html.Html msg) , description : Maybe (List (Html.Html msg)) } group : GroupProps InternalMsg -> List (Html.Html InternalMsg) -> Html.Html InternalMsg group { title } children = group { title, description } children = node "x-field-group" [] (p [ attribute "slot" "title" ] title :: children) (p [ attribute "slot" "title" ] title :: (description |> Maybe.map (p [ attribute "slot" "description" ]) |> Maybe.withDefault (text "")) :: children ) view : Model -> Parameters -> List (Html.Attribute InternalMsg) -> Html.Html Msg view model params attrs = node "x-parameters" attrs [ group { title = [ text "General" ] } [ field model { key = LugWidth , title = [ text "Lug width" ] , description = [ text "This will be the final width of your strap. " , text "You can use a size smaller than your lug width to create a play." ] , unit = Just "mm" , attrs = step "1.0" :: lengthFieldAttrs constraints.lugWidth } ] , hr [] [] , group { title = [ text "Buckle / Clasp" ] } [ field model { key = BuckleHoleCount , title = [ text "Hole Count" ] , description = [ text "Set 0 to disable buckle holes generation." ] , unit = Nothing , attrs = step "1" :: intFieldAttrs constraints.longPiece.buckleHole.count } , field model { key = BuckleHoleDiameter , title = [ text "Hole Diameter" ] , description = [ text "Diameter of buckle holes. You can leave the default value if you're going to use the center mark." ] , unit = Just "mm" , attrs = step "1.0" :: disabled (params.longPiece.buckleHole.count == 0) :: lengthFieldAttrs constraints.longPiece.buckleHole.diameter } ] , hr [] [] , group { title = [ text "Rendering" ] } [ field model { key = CanvasMargin , title = [ text "Print Margin" ] , description = [ text "Lower values can cause printing problems depending on your printer." ] , unit = Just "mm" , attrs = step "1.0" :: lengthFieldAttrs constraints.rendering.margin } , field model { key = LineWidth , title = [ text "Line Width" ] , description = [ text "Stroke width (thickness) of the cutting lines and seam lines." ] , unit = Just "mm" , attrs = step "0.1" :: lengthFieldAttrs constraints.rendering.lineWidth } ] view : Model -> Parameters -> List (Html.Html Msg) view model params = [ group { title = [ text "General" ], description = Nothing } [ field model { key = LugWidth , title = [ text "Lug width" ] , description = [ text "This will be the final width of your strap. " , text "You can use a size smaller than your lug width to create a play." ] , unit = Just "mm" , attrs = step "1.0" :: lengthFieldAttrs constraints.lugWidth } ] , hr [] [] , group { title = [ text "Buckle / Clasp" ], description = Nothing } [ field model { key = BuckleHoleCount , title = [ text "Hole Count" ] , description = [ text "Set 0 to disable buckle holes generation." ] , unit = Nothing , attrs = step "1" :: intFieldAttrs constraints.longPiece.buckleHole.count } , field model { key = BuckleHoleDiameter , title = [ text "Hole Diameter" ] , description = [ text "Diameter of buckle holes. You can leave the default value if you're going to use the center mark." ] , unit = Just "mm" , attrs = step "1.0" :: disabled (params.longPiece.buckleHole.count == 0) :: lengthFieldAttrs constraints.longPiece.buckleHole.diameter } ] , hr [] [] , group { title = [ text "Rendering" ], description = Nothing } [ field model { key = CanvasMargin , title = [ text "Print Margin" ] , description = [ text "Lower values can cause printing problems depending on your printer." ] , unit = Just "mm" , attrs = step "1.0" :: lengthFieldAttrs constraints.rendering.margin } , field model { key = LineWidth , title = [ text "Line Width" ] , description = [ text "Stroke width (thickness) of the cutting lines and seam lines." ] , unit = Just "mm" , attrs = step "0.1" :: lengthFieldAttrs constraints.rendering.lineWidth } ] |> Html.map Internal ] |> List.map (Html.map Internal)
-
-
src/Preferences.elm (new)
-
@@ -0,0 +1,75 @@-- Copyright 2026 Shota FUJI -- -- This Source Code Form is subject to the terms of the Mozilla Public -- License, v. 2.0. If a copy of the MPL was not distributed with this -- file, You can obtain one at https://mozilla.org/MPL/2.0/. -- -- SPDX-License-Identifier: MPL-2.0 module Preferences exposing (Preferences, PreviewTheme(..), decoder, defaultPreferences, encode) import Json.Decode as Decode import Json.Encode as Encode type PreviewTheme = SystemTheme | PrintTheme previewThemeDecoder : Decode.Decoder PreviewTheme previewThemeDecoder = Decode.string |> Decode.andThen (\str -> case str of "system" -> Decode.succeed SystemTheme "print" -> Decode.succeed PrintTheme _ -> Decode.fail ("\"" ++ str ++ "\" is not a valid PreviewTheme") ) encodePreviewTheme : PreviewTheme -> Encode.Value encodePreviewTheme theme = Encode.string (case theme of SystemTheme -> "system" PrintTheme -> "print" ) {-| User preferences. This is not supposed to be shared. -} type alias Preferences = { previewTheme : PreviewTheme } defaultPreferences : Preferences defaultPreferences = { previewTheme = SystemTheme } decoder : Decode.Decoder Preferences decoder = Decode.map Preferences (Decode.field "preview_theme" (Decode.oneOf [ previewThemeDecoder, Decode.succeed defaultPreferences.previewTheme ]) ) encode : Preferences -> Encode.Value encode p = Encode.object [ ( "preview_theme", encodePreviewTheme p.previewTheme ) ]
-
-
src/Preferences/App.elm (new)
-
@@ -0,0 +1,163 @@-- Copyright 2026 Shota FUJI -- -- This Source Code Form is subject to the terms of the Mozilla Public -- License, v. 2.0. If a copy of the MPL was not distributed with this -- file, You can obtain one at https://mozilla.org/MPL/2.0/. -- -- SPDX-License-Identifier: MPL-2.0 port module Preferences.App exposing (Model, Msg, init, panelItems, update) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events import Json.Decode as Decode import Json.Encode as Encode import Preferences exposing (Preferences, PreviewTheme(..)) -- PORTS port writePreferences : Encode.Value -> Cmd msg -- MODEL type alias Model = { preferences : Preferences } init : Decode.Value -> Model init saved = case Decode.decodeValue Preferences.decoder saved of Ok preferences -> { preferences = preferences } Err _ -> -- TODO: Notify decode error { preferences = Preferences.defaultPreferences } -- UPDATE type Msg = NoOp | SetPreviewTheme PreviewTheme update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = let { preferences } = model in case msg of NoOp -> ( model, Cmd.none ) SetPreviewTheme theme -> let nextPreferences = { preferences | previewTheme = theme } in ( { model | preferences = { preferences | previewTheme = theme } } , writePreferences (Preferences.encode nextPreferences) ) -- VIEW previewThemeId : String previewThemeId = "preview-theme" descriptionId : String -> String descriptionId prefix = prefix ++ "_description" type alias RadioBoxProps msg = { label : List (Html msg) , description : List (Html msg) , name : String , value : String , checked : Bool , onCheck : msg } radioBox : RadioBoxProps msg -> List (Html.Attribute msg) -> Html msg radioBox { label, description, name, value, checked, onCheck } attrs = let id = name ++ "_" ++ value in node "x-radio-box" [ if checked then attribute "checked" "" else class "" ] [ Html.label [ for id, attribute "slot" "label" ] label , Html.p [ Html.Attributes.id (descriptionId id), attribute "slot" "description" ] description , input (type_ "radio" :: Html.Attributes.id id :: Html.Attributes.name name :: Html.Attributes.value value :: Html.Attributes.checked checked :: Html.Events.onCheck (\_ -> onCheck) :: attrs ) [] ] panelItems : Model -> List (Html Msg) panelItems model = [ node "x-field-group" [] [ p [ attribute "slot" "title" ] [ text "Preferences" ] , p [ attribute "slot" "description" ] [ text "These parameters are saved to this browser, and will not be shared." ] , node "x-field" [] [ span [ attribute "slot" "title" ] [ text "Preview Color Theme" ] , p [ attribute "slot" "description" , id (descriptionId previewThemeId) ] [ text "This does not affect print output." ] , radioBox { label = [ text "Use system theme" ] , description = [ text "Toggle light and dark mode automatically based on your system's dark mode setting." ] , name = previewThemeId , value = "system" , checked = model.preferences.previewTheme == SystemTheme , onCheck = SetPreviewTheme SystemTheme } [] , radioBox { label = [ text "Use print theme" ] , description = [ text "Preview in printed color. Output image will be exactly same to the preview." ] , name = previewThemeId , value = "print" , checked = model.preferences.previewTheme == PrintTheme , onCheck = SetPreviewTheme PrintTheme } [] ] ] ]
-
-
-
@@ -13,6 +13,7 @@ import { XNumberInput } from "./x-number-input.js";import { XPanel } from "./x-panel.js"; import { XParameters } from "./x-parameters.js"; import { XPreview } from "./x-preview.js"; import { XRadioBox } from "./x-radio-box.js"; customElements.define("x-app-layout", XAppLayout); customElements.define("x-field", XField);
-
@@ -21,3 +22,4 @@ customElements.define("x-number-input", XNumberInput);customElements.define("x-panel", XPanel); customElements.define("x-parameters", XParameters); customElements.define("x-preview", XPreview); customElements.define("x-radio-box", XRadioBox);
-
-
-
@@ -23,3 +23,10 @@ ::slotted([slot="title"]) {font-weight: 300; font-size: 1.2rem; } ::slotted([slot="description"]) { padding: 0.5em 0; font-size: 0.8rem; opacity: 0.8; }
-
-
-
@@ -28,6 +28,10 @@ const title = document.createElement("slot");title.name = "title"; header.appendChild(title); const description = document.createElement("slot"); description.name = "description"; header.appendChild(description); const slot = document.createElement("slot"); shadow.appendChild(slot); }
-
-
-
@@ -19,9 +19,8 @@ font-weight: bold;font-size: 0.9rem; } ::slotted(input) { border: 1px solid ButtonBorder; border-radius: 1px; ::slotted(x-radio-box) { margin: 0.1rem -0.5rem !important; } ::slotted(input[aria-invalid="true"]) {
-
-
-
@@ -0,0 +1,74 @@/* * Copyright 2026 Shota FUJI * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. * * SPDX-License-Identifier: MPL-2.0 */ :host { position: relative; border: 1px solid ButtonFace; padding: 0.5rem; padding-inline-start: 1.2rem; padding-bottom: 0.25rem; border-radius: 3px; } :host([checked]) { border-color: ButtonBorder; } :host(:focus-within) { border-color: Highlight; box-shadow: 0 0 0 2px Highlight; } ::slotted([slot="label"]) { font-size: 0.9rem; border-bottom: 1px solid transparent; opacity: 0.9; } ::slotted([slot="label"])::before { display: inline-block; content: ""; width: 0.3em; height: 0.3em; margin-inline-start: -0.7em; margin-inline-end: 0.4em; background-color: var(--_indicator-color, transparent); border-radius: 50%; vertical-align: bottom; transform: translateY(-130%); } :host([checked]) ::slotted([slot="label"]) { --_indicator-color: currentColor; opacity: 1; } :host(:hover) ::slotted([slot="label"]) { border-color: CanvasText; } ::slotted([slot="description"]) { padding: 0.5em 0; font-size: 0.8rem; opacity: 0.8; } ::slotted(input[type="radio"]) { position: absolute; inset: 0; opacity: 0; }
-
-
-
@@ -0,0 +1,34 @@// Copyright 2026 Shota FUJI // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // SPDX-License-Identifier: MPL-2.0 import css from "./x-radio-box.css"; export class XRadioBox extends HTMLElement { constructor() { super(); const shadow = this.attachShadow({ mode: "open", }); const style = document.createElement("style"); style.textContent = css; shadow.appendChild(style); const label = document.createElement("slot"); label.name = "label"; shadow.appendChild(label); const description = document.createElement("slot"); description.name = "description"; shadow.appendChild(description); const slot = document.createElement("slot"); shadow.appendChild(slot); } }
-
-
-
@@ -16,8 +16,32 @@ <meta name="color-scheme" content="light dark" /><title>Watch Strap Template Builder</title> <script src="/main.js"></script> <script type="module"> const PREFERENCES_KEY = "wwstb.preferences"; function loadPreferences() { try { const loaded = localStorage.getItem(PREFERENCES_KEY); if (!loaded) { return {}; } return JSON.parse(loaded); } catch (err) { console.warn("Failed to load stored preferences, falling back to defaults.", err); return {}; } } import("./elements.js").then(() => { Elm.Main.init(); const app = Elm.Main.init({ flags: { preferences: loadPreferences(), } }); app.ports.writePreferences.subscribe(preferences => { localStorage.setItem(PREFERENCES_KEY, JSON.stringify(preferences)); }); }); </script> <style>
-