├── .config └── dotnet-tools.json ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── Sutil.Generator.sln ├── build.ps1 └── src ├── Generator ├── Fast.fs ├── Generation.fs ├── Generator.fsproj ├── Program.fs ├── Shoelace.fs ├── Types.fs ├── package.json └── pnpm-lock.yaml └── website ├── .editorconfig ├── .firebaserc ├── .gitignore ├── CHANGELOG.md ├── README.md ├── firebase.json ├── markdown.pl.js ├── package.json ├── pnpm-lock.yaml ├── public ├── fable.ico └── index.html ├── snowpack.config.js └── src ├── App.fs ├── App.fsproj ├── Components └── Sidenav.fs ├── Main.fs ├── Pages ├── Docs.fs ├── Fast.fs ├── Home.fs └── Shoelace.fs ├── Router.fs ├── Router.js ├── Routes.fs ├── Theme.js ├── Types.fs ├── docs ├── fast │ ├── getting-started.md │ ├── index.md │ └── themes.md ├── getting-started.md ├── home-2.md ├── home.md └── shoelace │ ├── components.md │ ├── elmish.md │ ├── getting-started.md │ ├── index.md │ └── stores.md └── styles.css /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "fable": { 6 | "version": "3.2.2", 7 | "commands": [ 8 | "fable" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # Tye 65 | .tye/ 66 | 67 | # StyleCop 68 | StyleCopReport.xml 69 | 70 | # Files built by Visual Studio 71 | *_i.c 72 | *_p.c 73 | *_h.h 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.iobj 78 | *.pch 79 | *.pdb 80 | *.ipdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *_wpftmp.csproj 91 | *.log 92 | *.vspscc 93 | *.vssscc 94 | .builds 95 | *.pidb 96 | *.svclog 97 | *.scc 98 | 99 | # Chutzpah Test files 100 | _Chutzpah* 101 | 102 | # Visual C++ cache files 103 | ipch/ 104 | *.aps 105 | *.ncb 106 | *.opendb 107 | *.opensdf 108 | *.sdf 109 | *.cachefile 110 | *.VC.db 111 | *.VC.VC.opendb 112 | 113 | # Visual Studio profiler 114 | *.psess 115 | *.vsp 116 | *.vspx 117 | *.sap 118 | 119 | # Visual Studio Trace Files 120 | *.e2e 121 | 122 | # TFS 2012 Local Workspace 123 | $tf/ 124 | 125 | # Guidance Automation Toolkit 126 | *.gpState 127 | 128 | # ReSharper is a .NET coding add-in 129 | _ReSharper*/ 130 | *.[Rr]e[Ss]harper 131 | *.DotSettings.user 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Coverlet is a free, cross platform Code Coverage Tool 144 | coverage*[.json, .xml, .info] 145 | 146 | # Visual Studio code coverage results 147 | *.coverage 148 | *.coveragexml 149 | 150 | # NCrunch 151 | _NCrunch_* 152 | .*crunch*.local.xml 153 | nCrunchTemp_* 154 | 155 | # MightyMoose 156 | *.mm.* 157 | AutoTest.Net/ 158 | 159 | # Web workbench (sass) 160 | .sass-cache/ 161 | 162 | # Installshield output folder 163 | [Ee]xpress/ 164 | 165 | # DocProject is a documentation generator add-in 166 | DocProject/buildhelp/ 167 | DocProject/Help/*.HxT 168 | DocProject/Help/*.HxC 169 | DocProject/Help/*.hhc 170 | DocProject/Help/*.hhk 171 | DocProject/Help/*.hhp 172 | DocProject/Help/Html2 173 | DocProject/Help/html 174 | 175 | # Click-Once directory 176 | publish/ 177 | 178 | # Publish Web Output 179 | *.[Pp]ublish.xml 180 | *.azurePubxml 181 | # Note: Comment the next line if you want to checkin your web deploy settings, 182 | # but database connection strings (with potential passwords) will be unencrypted 183 | *.pubxml 184 | *.publishproj 185 | 186 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 187 | # checkin your Azure Web App publish settings, but sensitive information contained 188 | # in these scripts will be unencrypted 189 | PublishScripts/ 190 | 191 | # NuGet Packages 192 | *.nupkg 193 | # NuGet Symbol Packages 194 | *.snupkg 195 | # The packages folder can be ignored because of Package Restore 196 | **/[Pp]ackages/* 197 | # except build/, which is used as an MSBuild target. 198 | !**/[Pp]ackages/build/ 199 | # Uncomment if necessary however generally it will be regenerated when needed 200 | #!**/[Pp]ackages/repositories.config 201 | # NuGet v3's project.json files produces more ignorable files 202 | *.nuget.props 203 | *.nuget.targets 204 | 205 | # Microsoft Azure Build Output 206 | csx/ 207 | *.build.csdef 208 | 209 | # Microsoft Azure Emulator 210 | ecf/ 211 | rcf/ 212 | 213 | # Windows Store app package directories and files 214 | AppPackages/ 215 | BundleArtifacts/ 216 | Package.StoreAssociation.xml 217 | _pkginfo.txt 218 | *.appx 219 | *.appxbundle 220 | *.appxupload 221 | 222 | # Visual Studio cache files 223 | # files ending in .cache can be ignored 224 | *.[Cc]ache 225 | # but keep track of directories ending in .cache 226 | !?*.[Cc]ache/ 227 | 228 | # Others 229 | ClientBin/ 230 | ~$* 231 | *~ 232 | *.dbmdl 233 | *.dbproj.schemaview 234 | *.jfm 235 | *.pfx 236 | *.publishsettings 237 | orleans.codegen.cs 238 | 239 | # Including strong name files can present a security risk 240 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 241 | #*.snk 242 | 243 | # Since there are multiple workflows, uncomment next line to ignore bower_components 244 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 245 | #bower_components/ 246 | 247 | # RIA/Silverlight projects 248 | Generated_Code/ 249 | 250 | # Backup & report files from converting an old project file 251 | # to a newer Visual Studio version. Backup files are not needed, 252 | # because we have git ;-) 253 | _UpgradeReport_Files/ 254 | Backup*/ 255 | UpgradeLog*.XML 256 | UpgradeLog*.htm 257 | ServiceFabricBackup/ 258 | *.rptproj.bak 259 | 260 | # SQL Server files 261 | *.mdf 262 | *.ldf 263 | *.ndf 264 | 265 | # Business Intelligence projects 266 | *.rdl.data 267 | *.bim.layout 268 | *.bim_*.settings 269 | *.rptproj.rsuser 270 | *- [Bb]ackup.rdl 271 | *- [Bb]ackup ([0-9]).rdl 272 | *- [Bb]ackup ([0-9][0-9]).rdl 273 | 274 | # Microsoft Fakes 275 | FakesAssemblies/ 276 | 277 | # GhostDoc plugin setting file 278 | *.GhostDoc.xml 279 | 280 | # Node.js Tools for Visual Studio 281 | .ntvs_analysis.dat 282 | node_modules/ 283 | 284 | # Visual Studio 6 build log 285 | *.plg 286 | 287 | # Visual Studio 6 workspace options file 288 | *.opt 289 | 290 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 291 | *.vbw 292 | 293 | # Visual Studio LightSwitch build output 294 | **/*.HTMLClient/GeneratedArtifacts 295 | **/*.DesktopClient/GeneratedArtifacts 296 | **/*.DesktopClient/ModelManifest.xml 297 | **/*.Server/GeneratedArtifacts 298 | **/*.Server/ModelManifest.xml 299 | _Pvt_Extensions 300 | 301 | # Paket dependency manager 302 | .paket/paket.exe 303 | paket-files/ 304 | 305 | # FAKE - F# Make 306 | .fake/ 307 | 308 | # Ionide - VsCode extension for F# Support 309 | .ionide/ 310 | 311 | # CodeRush personal settings 312 | .cr/personal 313 | 314 | # Python Tools for Visual Studio (PTVS) 315 | __pycache__/ 316 | *.pyc 317 | 318 | # Cake - Uncomment if you are using it 319 | # tools/** 320 | # !tools/packages.config 321 | 322 | # Tabs Studio 323 | *.tss 324 | 325 | # Telerik's JustMock configuration file 326 | *.jmconfig 327 | 328 | # BizTalk build output 329 | *.btp.cs 330 | *.btm.cs 331 | *.odx.cs 332 | *.xsd.cs 333 | 334 | # OpenCover UI analysis results 335 | OpenCover/ 336 | 337 | # Azure Stream Analytics local run output 338 | ASALocalRun/ 339 | 340 | # MSBuild Binary and Structured Log 341 | *.binlog 342 | 343 | # NVidia Nsight GPU debugger configuration file 344 | *.nvuser 345 | 346 | # MFractors (Xamarin productivity tool) working folder 347 | .mfractor/ 348 | 349 | # Local History for Visual Studio 350 | .localhistory/ 351 | 352 | # BeatPulse healthcheck temp database 353 | healthchecksdb 354 | 355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 356 | MigrationBackup/ 357 | 358 | # Ionide (cross platform F# VS Code tools) working folder 359 | .ionide/ 360 | 361 | ## 362 | ## Visual studio for Mac 363 | ## 364 | 365 | 366 | # globs 367 | Makefile.in 368 | *.userprefs 369 | *.usertasks 370 | config.make 371 | config.status 372 | aclocal.m4 373 | install-sh 374 | autom4te.cache/ 375 | *.tar.gz 376 | tarballs/ 377 | test-results/ 378 | 379 | # Mac bundle stuff 380 | *.dmg 381 | *.app 382 | 383 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 384 | # General 385 | .DS_Store 386 | .AppleDouble 387 | .LSOverride 388 | 389 | # Icon must end with two \r 390 | Icon 391 | 392 | 393 | # Thumbnails 394 | ._* 395 | 396 | # Files that might appear in the root of a volume 397 | .DocumentRevisions-V100 398 | .fseventsd 399 | .Spotlight-V100 400 | .TemporaryItems 401 | .Trashes 402 | .VolumeIcon.icns 403 | .com.apple.timemachine.donotpresent 404 | 405 | # Directories potentially created on remote AFP share 406 | .AppleDB 407 | .AppleDesktop 408 | Network Trash Folder 409 | Temporary Items 410 | .apdisk 411 | 412 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 413 | # Windows thumbnail cache files 414 | Thumbs.db 415 | ehthumbs.db 416 | ehthumbs_vista.db 417 | 418 | # Dump file 419 | *.stackdump 420 | 421 | # Folder config file 422 | [Dd]esktop.ini 423 | 424 | # Recycle Bin used on file shares 425 | $RECYCLE.BIN/ 426 | 427 | # Windows Installer files 428 | *.cab 429 | *.msi 430 | *.msix 431 | *.msm 432 | *.msp 433 | 434 | # Windows shortcuts 435 | *.lnk 436 | 437 | # JetBrains Rider 438 | .idea/ 439 | *.sln.iml 440 | 441 | ## 442 | ## Visual Studio Code 443 | ## 444 | .vscode/* 445 | !.vscode/settings.json 446 | !.vscode/tasks.json 447 | !.vscode/launch.json 448 | !.vscode/extensions.json 449 | 450 | 451 | Sutil.Shoelace 452 | Sutil.Fast -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Run Shoelace", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | "program": "${workspaceFolder}/src/Generator/bin/Debug/net5.0/Generator.dll", 13 | "args": [ 14 | "-cs", 15 | "shoelace" 16 | ], 17 | "cwd": "${workspaceFolder}/src/Generator", 18 | "stopAtEntry": false, 19 | "console": "internalConsole" 20 | }, 21 | { 22 | "name": "Run Fast", 23 | "type": "coreclr", 24 | "request": "launch", 25 | "preLaunchTask": "build", 26 | "program": "${workspaceFolder}/src/Generator/bin/Debug/net5.0/Generator.dll", 27 | "args": [ 28 | "-cs", 29 | "fast" 30 | ], 31 | "cwd": "${workspaceFolder}/src/Generator", 32 | "stopAtEntry": false, 33 | "console": "internalConsole" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | // Ask dotnet build to generate full paths for file names. 13 | "/property:GenerateFullPaths=true", 14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 15 | "/consoleloggerparameters:NoSummary" 16 | ], 17 | "group": "build", 18 | "presentation": { 19 | "reveal": "silent" 20 | }, 21 | "problemMatcher": "$msCompile" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Shoelace.Sutil Documentation 2 | Sutil.Shoelace Docs are in the `website` directory at the root of the project. 3 | 4 | To be able to update existing documents or add them you'll need to: 5 | 1. Create a new `markdown` file at `website/src/docs/` in case it doesn't exist. 6 | 7 | Example: `website/src/docs/sl-button.md` 8 | 2. Add/Edit the contents of the document. 9 | 10 | you can use markdown or html including script tags. Example: 11 | 12 | ```markdown 13 | # My Article 14 | My explanation of how an SlButton works 15 | '''fsharp 16 | Shoelace.SlButton [ 17 | type' "primary" 18 | onClick (fun _ -> printfn "clicked") [] 19 | text "I'm a button" 20 | ] 21 | ''' 22 | then a small sample on how would the result be (which can't be done with F# in the website yet) 23 | 24 | I'm a button 25 | 29 | ``` 30 | 31 | 3. Once this is done the file will be available at `#/docs/sl-button`. 32 | 33 | 4. If you feel the need for it add an entry on the menu at `website/src/Routes.fs`. 34 | 35 | In this example you would add something like this inside the routes list 36 | ```fsharp 37 | { name = "SlButton" 38 | href = "#/docs/sl-button" 39 | category = Components } 40 | ``` 41 | 42 | 43 | # Shoelace.Generator project 44 | 45 | The generator project does the following 46 | 47 | 1. Download the `@shoelace-style/shoelace` package from the npm registry 48 | 2. Reads `./node_modules/@shoelace-style/shoelace/dist/metadata.json` 49 | 3. Deserializes it's contents into the types available in `Types.fs` 50 | 4. Writes a complete F# project to `./Sutil.Shoelace` including the `.fsproj` file 51 | 52 | Each file that is generated corresponds to a component that exists inside the `metadata.json` 53 | 54 | If you want to add functionality to the modules/components then you'll need to write the corresponding string template in `Templates.fs` the file is quite messy right now but any help to improve the clarity in this file is welcome. 55 | 56 | 57 | ## Getting to know the end product 58 | For the next section I'll talk about what is expected to have on the generated output although, for brevity I'll take out doc comments written in the final files and put my own comments instead 59 | 60 | ## Anatomy of a generated file (with attributes) 61 | 62 | 63 | ```fsharp 64 | // declation of the module and open statements 65 | module Sutil.Shoelace.MenuItem 66 | open Browser.Types 67 | open Sutil 68 | open Sutil.DOM 69 | 70 | (* Every Shoelace component has a native HTML element ideally we need 71 | to provide bindings of these so there is typing information 72 | when the users access them at event handlers 73 | *) 74 | [] 75 | type SlMenuItem = 76 | inherit HTMLElement 77 | (* there are certain reserved words from F# that are often used in javascript 78 | in these cases we use double backticks " ` " to preserve the original names *) 79 | abstract member ``checked`` : bool with get, set 80 | abstract member disabled : bool with get, set 81 | abstract member value : string with get, set 82 | abstract member blur : unit 83 | (* in the cases we know what the parameters are we should provide anonymous types for them 84 | the reason being anonymous types get translated as plain javascript objects in Fable 85 | if the type is not known or problematic then obj should be good enough *) 86 | abstract member focus : {| preventScroll: bool option |} option -> unit 87 | 88 | (* IF the component has documented attributes we'll emit a type and module 89 | named like this `SlAttributes` 90 | this is to provide a few helpers described below *) 91 | type SlMenuItemAttributes = { 92 | checked' : bool option 93 | disabled : bool option 94 | value : string option 95 | } 96 | 97 | [] 98 | module SlMenuItemAttributes = 99 | (* every attributes module will have a created function 100 | that provides an object with optional values and for some 101 | of them their default values *) 102 | let create(): SlMenuItemAttributes = { 103 | checked' = Some false 104 | disabled = Some false 105 | value = Some "" 106 | } 107 | 108 | (* from there on we'll provide simple functions in the form of 109 | `with (: type) (attrs: attribute type)` 110 | in the case of reserved names since these are handled in the library code 111 | we can use ` ' ` to sufix the property name and this will not impact the HTMLELement *) 112 | 113 | let withChecked (checked': bool) (attrs: SlMenuItemAttributes) = 114 | { attrs with checked' = Some checked' } 115 | 116 | let withDisabled (disabled: bool) (attrs: SlMenuItemAttributes) = 117 | { attrs with disabled = Some disabled } 118 | 119 | 120 | let withValue (value: string) (attrs: SlMenuItemAttributes) = 121 | { attrs with value = Some value } 122 | (* We'll provide a module with the name of the component 123 | and two methods in the case of components with attributes *) 124 | [] 125 | module SlMenuItem = 126 | 127 | (* stateless is a bad name since it's not stateless 128 | the idea behind is that we're not providing any kind of binding by default 129 | and we're just providing a simple function that returns an html element *) 130 | let stateless (content: NodeFactory seq) = Html.custom("sl-menu-item", content) 131 | (* The "stateful" function provides a simple mechanism to bind all of the attributes 132 | to the html element via observables and `Bind.attr(, )` where "ATTRIBUTE_NAME" is the actual html attribute (checked, disabled, no-fieldset (Note: dash cased instead of cammel case when writing these attributes)) 133 | then we just call the stateless function and yield everything to the Html.custom function from Sutil 134 | *) 135 | let stateful (attrs: IStore) (nodes: NodeFactory seq) = 136 | 137 | let checked' = attrs .> (fun attrs -> attrs.checked') 138 | let disabled = attrs .> (fun attrs -> attrs.disabled) 139 | let value = attrs .> (fun attrs -> attrs.value) 140 | stateless 141 | [ Bind.attr("checked", checked') 142 | Bind.attr("disabled", disabled) 143 | Bind.attr("value", value) 144 | yield! nodes ] 145 | ``` 146 | 147 | ## Anatomy of a generated file (without attributes) 148 | 149 | 150 | ```fsharp 151 | // declation of the module and open statements 152 | module Sutil.Shoelace.MenuLabel 153 | open Browser.Types 154 | open Sutil 155 | open Sutil.DOM 156 | (* Every Shoelace component has a native HTML element ideally we need 157 | to provide bindings of these so there is typing information 158 | when the users access them at event handlers 159 | *) 160 | [] 161 | type SlMenuLabel = 162 | inherit HTMLElement 163 | 164 | 165 | (* We'll provide a module with the name of the component 166 | In cases like this that the components don't provide documented attributes 167 | we just emit the stateless function *) 168 | [] 169 | module SlMenuLabel = 170 | 171 | let stateless (content: NodeFactory seq) = Html.custom("sl-menu-label", content) 172 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Angel D. Munoz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # STATUS UPDATE 2 | 3 | The shoelace spec has changed a little bit and this generator won't work anymore, there is a more standards compatible metadata format which is here 4 | https://github.com/webcomponents/custom-elements-manifest if you want to make this a reality ping me on twitter @angel_d_munoz, if we can generate an F# AST from that metadata then Fantomas can write the code for us rather than us using strings 5 | 6 | 7 | This project for the moment will be put to rest in the meantime 8 | 9 | 10 | # Sutil.Generator 11 | 12 | This is a [Shoelace](https://github.com/shoelace-style/shoelace) and [Fast](https://fast.design) wrapper generator for [Sutil](https://github.com/davedawkins/Sutil) heavily inspider in [react-generator](https://github.com/shoelace-style/react-generator) 13 | 14 | # Generate `Sutil.Shoelace` or `Sutil.Fast` project 15 | To generate the `Sutil.Shoelace` or `Sutil.Fast` project you will need to have node installed in your machine. 16 | - We download the `@shoelace-style/shoelace` package which contains a `metadata.json` file that allows us to automate the generation of the F# source code. 17 | > In the case of Fast we read each component's metadata file to do the file generation 18 | - Once the package is downloaded the project proceeds to generate one file for each component listed in the metadata file 19 | 20 | To kick off these events run 21 | ```sh 22 | ./build.ps1 fast 23 | ``` 24 | Normally you would do this to either 25 | 26 | 1. Draft a new release 27 | 2. Use Sutil.Shoelace or Sutil.Fast to improve the docs website 28 | 29 | 30 | # Future Ideas 31 | 32 | - [x] ~Propose a json schema to different libraries~ There's already a [Custom Elements Manifest](https://github.com/open-wc/custom-elements-manifest) 33 | - [ ] Support Custom Elements Manifest to generate libraries agnostically 34 | - [ ] Decouple Shoelace to the generator 35 | - [ ] Decouple Sutil from the generation phase (e.g allow this generator to create Feliz components or other DSL Flavor / Library) 36 | -------------------------------------------------------------------------------- /Sutil.Generator.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F359596B-74AA-488C-82C8-187DCF72C281}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Generator", "src\Generator\Generator.fsproj", "{B41BFB87-1949-435B-B89B-2B49C9DE7C8B}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "website", "website", "{550A6B28-0B40-4579-9B26-72EF75BCC632}" 11 | EndProject 12 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "App", "src\website\src\App.fsproj", "{4A995969-F98A-4B29-BE11-295DD45AC617}" 13 | EndProject 14 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Sutil.Shoelace", "src\Sutil.Shoelace\Sutil.Shoelace.fsproj", "{F6C84529-7F46-4A2B-B466-9E418BC5958E}" 15 | EndProject 16 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Sutil.Fast", "src\Sutil.Fast\Sutil.Fast.fsproj", "{756D5AF7-4F50-4DDA-95D9-38C825CA57B7}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|x64 = Release|x64 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Debug|x64.ActiveCfg = Debug|Any CPU 34 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Debug|x64.Build.0 = Debug|Any CPU 35 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Debug|x86.ActiveCfg = Debug|Any CPU 36 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Debug|x86.Build.0 = Debug|Any CPU 37 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Release|x64.ActiveCfg = Release|Any CPU 40 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Release|x64.Build.0 = Release|Any CPU 41 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Release|x86.ActiveCfg = Release|Any CPU 42 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B}.Release|x86.Build.0 = Release|Any CPU 43 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Debug|x64.ActiveCfg = Debug|Any CPU 46 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Debug|x64.Build.0 = Debug|Any CPU 47 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Debug|x86.ActiveCfg = Debug|Any CPU 48 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Debug|x86.Build.0 = Debug|Any CPU 49 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Release|x64.ActiveCfg = Release|Any CPU 52 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Release|x64.Build.0 = Release|Any CPU 53 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Release|x86.ActiveCfg = Release|Any CPU 54 | {4A995969-F98A-4B29-BE11-295DD45AC617}.Release|x86.Build.0 = Release|Any CPU 55 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Debug|x64.ActiveCfg = Debug|Any CPU 58 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Debug|x64.Build.0 = Debug|Any CPU 59 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Debug|x86.ActiveCfg = Debug|Any CPU 60 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Debug|x86.Build.0 = Debug|Any CPU 61 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Release|x64.ActiveCfg = Release|Any CPU 64 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Release|x64.Build.0 = Release|Any CPU 65 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Release|x86.ActiveCfg = Release|Any CPU 66 | {F6C84529-7F46-4A2B-B466-9E418BC5958E}.Release|x86.Build.0 = Release|Any CPU 67 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 68 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Debug|Any CPU.Build.0 = Debug|Any CPU 69 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Debug|x64.ActiveCfg = Debug|Any CPU 70 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Debug|x64.Build.0 = Debug|Any CPU 71 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Debug|x86.ActiveCfg = Debug|Any CPU 72 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Debug|x86.Build.0 = Debug|Any CPU 73 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Release|Any CPU.ActiveCfg = Release|Any CPU 74 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Release|Any CPU.Build.0 = Release|Any CPU 75 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Release|x64.ActiveCfg = Release|Any CPU 76 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Release|x64.Build.0 = Release|Any CPU 77 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Release|x86.ActiveCfg = Release|Any CPU 78 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7}.Release|x86.Build.0 = Release|Any CPU 79 | EndGlobalSection 80 | GlobalSection(NestedProjects) = preSolution 81 | {B41BFB87-1949-435B-B89B-2B49C9DE7C8B} = {F359596B-74AA-488C-82C8-187DCF72C281} 82 | {550A6B28-0B40-4579-9B26-72EF75BCC632} = {F359596B-74AA-488C-82C8-187DCF72C281} 83 | {4A995969-F98A-4B29-BE11-295DD45AC617} = {550A6B28-0B40-4579-9B26-72EF75BCC632} 84 | {756D5AF7-4F50-4DDA-95D9-38C825CA57B7} = {F359596B-74AA-488C-82C8-187DCF72C281} 85 | EndGlobalSection 86 | EndGlobal 87 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | param ( 3 | [Parameter(Mandatory = $true)] 4 | [string] 5 | $library = "shoelace" 6 | ) 7 | 8 | $project = $library -eq "shoelace" ? "Sutil.Shoelace" : "Sutil.Fast" 9 | 10 | $root = Get-Location 11 | 12 | 13 | Remove-Item $library -R -Force -ErrorAction Ignore; 14 | Remove-Item dist -R -Force -ErrorAction Ignore; 15 | dotnet tool restore 16 | set-location $root/src/Generator 17 | dotnet run -C Release -- -cs $library 18 | if ($LASTEXITCODE -gt 0) { 19 | Exit 1 20 | } 21 | dotnet build 22 | if ($LASTEXITCODE -gt 0) { 23 | Exit 1 24 | } 25 | Set-Location $root 26 | dotnet fable --cwd src/$project 27 | if ($LASTEXITCODE -gt 0) { 28 | Exit 1 29 | } 30 | dotnet pack src/$project -o dist -------------------------------------------------------------------------------- /src/Generator/Fast.fs: -------------------------------------------------------------------------------- 1 | namespace Sutil.Generator.Fast 2 | 3 | 4 | (* 5 | DO NOT USE FANTOMAS OR ANOTHER FORMATTER 6 | THAT CHANGES THE MULTI-LINE STRING STRUCTURE ON THIS FILE 7 | IT WILL BREAK THE DOC COMMENTS GENERATION 8 | *) 9 | 10 | 11 | open System 12 | open Sutil.Generator.Types 13 | open System.Web 14 | open System.Text.RegularExpressions 15 | 16 | 17 | type FastPackageJson = 18 | FSharp.Data.JsonProvider<"node_modules/@microsoft/fast-components/package.json", InferTypesFromValues=false> 19 | 20 | [] 21 | module Templates = 22 | let private getUpercasedName (name: string) (includefirstWord: bool) (cammelCase: bool) = 23 | 24 | let inline pascalCase (i: int) (word: string) = 25 | if i = 0 && cammelCase then 26 | word.[0] 27 | else 28 | Char.ToUpperInvariant(word.[0]) 29 | 30 | let upercased = 31 | name.Split('-') 32 | let upercased = 33 | if includefirstWord then 34 | upercased |> Array.mapi (fun i word -> $"{pascalCase i word}{word.[1..]}") 35 | else 36 | upercased |> Array.tail |> Array.mapi (fun i word -> $"{pascalCase i word}{word.[1..]}") 37 | String.Join("", upercased) 38 | 39 | let getAttributeName (name: string) = 40 | match name with 41 | | "type" -> "``type``" 42 | | "open" -> "``open``" 43 | | "inline" -> "``inline``" 44 | | "checked" -> "``checked``" 45 | | rest when rest.Contains('-') -> 46 | getUpercasedName rest true true 47 | | rest -> rest 48 | 49 | let getAttributeType (type': string) = 50 | match type' with 51 | | "boolean" -> "bool" 52 | | "void" -> "unit" 53 | | "number" -> "float" 54 | | type' when type'.StartsWith('{') || type'.EndsWith('}') -> "obj" 55 | | type' -> type' 56 | 57 | let getDefaultValue (value: string) = 58 | match value with 59 | | "undefined" 60 | | "null" 61 | | "" 62 | | null -> "None" 63 | | value -> 64 | let regex = new Regex(@"^[+-]?(([1-9][0-9]*)?[0-9](\.[0-9]*)?|\.[0-9]+)$") 65 | match value with 66 | | "true" 67 | | "false" -> 68 | $"""Some {value}""" 69 | | value when regex.IsMatch (value) -> 70 | $"""Some {value}""" 71 | | value -> $"""Some "{value}" """ 72 | 73 | let getDescription (description: string option) = 74 | match description with 75 | | Some description -> 76 | HttpUtility.HtmlEncode(description.Replace("\n", "\n /// ")) 77 | | None -> "" 78 | 79 | 80 | let private getFastAttr (prop: AttributeVscodeDefinition) = 81 | let name = getAttributeName prop.name 82 | let type' = getAttributeType prop.``type`` 83 | 84 | let description = getDescription prop.description 85 | 86 | $""" /// {description} 87 | abstract member {name} : {type'} with get, set""" 88 | let private getFastElementAttributes (props: AttributeVscodeDefinition seq) = 89 | props 90 | |> Seq.fold (fun (current: string) (next: AttributeVscodeDefinition) -> $"{current}\n{getFastAttr next}") "" 91 | 92 | let private getFastAttributesTypeTpl (prop: AttributeVscodeDefinition) = 93 | let name = getAttributeName prop.name 94 | let type' = getAttributeType prop.``type`` 95 | 96 | let description = getDescription prop.description 97 | 98 | $""" /// {description} 99 | {name} : {type'} option""" 100 | 101 | let private getAttrs (props: AttributeVscodeDefinition seq) = 102 | props 103 | |> Seq.fold (fun (current: string) (next: AttributeVscodeDefinition) -> $"{current}\n{getFastAttributesTypeTpl next}") "" 104 | 105 | 106 | let getSlotList (slots: SlotVsCodeDefinition seq) (padding: int option) = 107 | let padding = defaultArg padding 0 108 | let padding = " " |> String.replicate padding 109 | 110 | let getSlots = 111 | slots 112 | |> Seq.fold 113 | (fun (current: string) next -> 114 | let description = getDescription next.description 115 | 116 | let name = 117 | if next.name.Length = 0 then 118 | "default" 119 | else 120 | next.name 121 | 122 | let current = 123 | if current.Length > 0 then 124 | $"{current}\n" 125 | else 126 | "" 127 | 128 | $"{padding}{current}{padding}/// - `{name}`: {description}") 129 | "" 130 | 131 | $"""{padding}/// Slots: 132 | {padding}{getSlots.Trim()}""" 133 | 134 | let getComponentCommentTpl (comp: TagVsCodeDefinition) (padding: int option) = 135 | let spacePadding = defaultArg padding 0 136 | let padding = " " |> String.replicate spacePadding 137 | $""" 138 | {padding}/// 139 | {padding}/// Title: {comp.title} 140 | {padding}/// 141 | {padding}/// Tag: {comp.name} 142 | {padding}/// 143 | {getSlotList comp.slots (Some spacePadding)} 144 | {padding}/// 145 | {padding}/// """ 146 | 147 | let getCompModule (comp: TagVsCodeDefinition) (attrs: AttributeVscodeDefinition seq) = 148 | let tag = comp.name 149 | let name = getUpercasedName comp.name true false 150 | 151 | let getBindingsTpl = 152 | let props = 153 | attrs 154 | |> Seq.fold 155 | (fun current next -> 156 | let name = getAttributeName next.name 157 | $"{current}\n let {name} = attrs .> (fun attrs -> attrs.{name})") 158 | "" 159 | 160 | let bindings = 161 | attrs 162 | |> Seq.fold 163 | (fun (current: string) next -> 164 | let name = getAttributeName next.name 165 | 166 | let tag = 167 | next.name 168 | 169 | let current = 170 | if current.Length > 0 then 171 | $"{current}\n " 172 | else 173 | "" 174 | 175 | $"{current}Bind.attr(\"{tag}\", {name})") 176 | "" 177 | 178 | if props.Length > 0 then 179 | $""" 180 | /// Provides all of the bindings available for the HTML element 181 | /// It leverages "Bind.attr("attribute-name", observable)" to provide reactive changes 182 | let stateful (attrs: IStore<{name}Attributes>) (nodes: NodeFactory seq) = 183 | {props} 184 | stateless 185 | [ {bindings} 186 | yield! nodes ]""" 187 | else 188 | "" 189 | $""" 190 | /// 191 | [] 192 | module {name} = 193 | /// doesn't provide any binding helper logic and allows the user to take full 194 | /// control over the HTML Element either to create static HTML or do custom bindings 195 | /// via "bindFragment" or "Bind.attr("", binding)" 196 | let stateless (content: NodeFactory seq) = Html.custom("{tag}", content) 197 | {getBindingsTpl}""" 198 | 199 | 200 | let getWithFunction (prop: AttributeVscodeDefinition) (attrsName: string) = 201 | let name = getUpercasedName prop.name true false 202 | let propAttr = getAttributeName prop.name 203 | let type' = getAttributeType prop.``type`` 204 | 205 | let valuesComment = 206 | let values = 207 | (prop.values |> Option.defaultValue Seq.empty) 208 | 209 | let description = getDescription prop.description 210 | 211 | $"\n /// {description}\n /// Default Value: {prop.``default``}\n /// Type: {prop.``type``}" 212 | 213 | $"""{valuesComment} 214 | let with{name} ({propAttr}: {type'}) (attrs: {attrsName}) = 215 | {{ attrs with {propAttr} = Some {propAttr} }}""" 216 | 217 | let private getAttrRecordMemberValue (prop: AttributeVscodeDefinition) = 218 | let name = getAttributeName prop.name 219 | let type' = getAttributeType prop.``type`` 220 | let defValue = 221 | prop.``default`` |> Option.defaultValue ("undefined" :> obj) 222 | let value = getDefaultValue (defValue.ToString().ToLowerInvariant()) 223 | 224 | let addDot = 225 | match type' with 226 | | "float" when value.Contains('.') && value <> "None" -> "" 227 | | "float" when value <> "None" -> "." 228 | | _ -> "" 229 | 230 | let value = 231 | match value with 232 | | "Some true" when type' = "string" -> "Some \"true\"" 233 | | "Some false" when type' = "string" -> "Some \"false\"" 234 | | value -> value 235 | 236 | $"{name} = {value}{addDot}" 237 | 238 | let getAttrModule (name: string) (attrs: AttributeVscodeDefinition seq) = 239 | let getAttrs = 240 | attrs 241 | |> Seq.fold (fun current next -> $"{current}\n {getAttrRecordMemberValue next}") "" 242 | 243 | let withFunctions = 244 | let attrName = $"{name}Attributes" 245 | 246 | attrs 247 | |> Seq.fold (fun current next -> $"{current}\n {getWithFunction next attrName}") "" 248 | 249 | $""" 250 | /// 251 | [] 252 | module {name}Attributes = 253 | let create(): {name}Attributes = {{ {getAttrs} 254 | }} 255 | {withFunctions}""" 256 | 257 | let getComponentTpl (comp: TagVsCodeDefinition) = 258 | let moduleName = getUpercasedName comp.name true false 259 | let props = getFastElementAttributes comp.attributes 260 | let attrs = getAttrs comp.attributes 261 | let hasAttributes = comp.attributes |> Seq.length > 0 262 | let attrsTpl = 263 | if hasAttributes then 264 | $"\n///\ntype {getUpercasedName comp.name true false}Attributes = {{ {attrs}\n}}" 265 | else 266 | "" 267 | 268 | let attrsModule = 269 | if hasAttributes then 270 | getAttrModule (getUpercasedName comp.name true false) comp.attributes 271 | else 272 | "" 273 | 274 | $""" 275 | module Sutil.Fast.{moduleName} 276 | open Browser.Types 277 | open Sutil 278 | open Sutil.DOM 279 | {getComponentCommentTpl comp None} 280 | [] 281 | type {getUpercasedName comp.name true false} = 282 | inherit HTMLElement 283 | {props} 284 | {attrsTpl}{attrsModule} 285 | {getCompModule comp comp.attributes}""" 286 | 287 | 288 | let getStatefulComponentTpl (comp: TagVsCodeDefinition) = 289 | if comp.attributes |> Seq.length > 0 then 290 | let name = getUpercasedName comp.name true false 291 | $"""{getComponentCommentTpl comp (Some 4)} 292 | static member inline {name} (attrs: IStore<{name}Attributes>, content: NodeFactory seq) = 293 | {name}.stateful attrs content""" 294 | else 295 | "" 296 | 297 | let getStatelessComponentTpl (comp: TagVsCodeDefinition) = 298 | let name = getUpercasedName comp.name true false 299 | $"""{getComponentCommentTpl comp (Some 4)} 300 | static member inline {name} (content: NodeFactory seq) = 301 | {name}.stateless content""" 302 | 303 | let getFastAPIClass (components: TagVsCodeDefinition array) = 304 | let opens = 305 | components 306 | |> Array.fold 307 | (fun (current: string) next -> 308 | let name = getUpercasedName next.name true false 309 | 310 | let current = 311 | if current.Length > 0 then 312 | $"{current}\n" 313 | else 314 | "" 315 | 316 | $"{current}open Sutil.Fast.{name}") 317 | "" 318 | 319 | let methods = 320 | components 321 | |> Array.fold 322 | (fun (current: string) next -> 323 | let current = 324 | if current.Length > 0 then 325 | $"{current}\n " 326 | else 327 | "" 328 | 329 | let stateful = getStatefulComponentTpl next 330 | 331 | let stateless = getStatelessComponentTpl next 332 | 333 | $"{current}{stateless}{stateful}") 334 | "" 335 | 336 | $"""namespace Sutil.Fast 337 | open Sutil 338 | open Sutil.DOM 339 | {opens} 340 | /// 341 | /// Provides a simple API to access all Shoelace Components available 342 | /// 343 | type Fast = 344 | {methods}""" 345 | 346 | 347 | let getFsFileReference (components: TagVsCodeDefinition array) = 348 | 349 | components 350 | |> Array.fold 351 | (fun (current: string) (next: TagVsCodeDefinition) -> 352 | $"{current}\n ") 353 | """""" 354 | 355 | let getFsProjTpl (comps: string) (package: string) (version: string) = 356 | $""" 357 | 358 | 359 | Sutil bindings for {package} Web Components. 360 | The contents of this package are auto-generated 361 | 362 | https://github.com/AngelMunoz/Sutil.Generator 363 | https://github.com/AngelMunoz/Sutil.Generator 364 | 365 | fsharp;fable;svelte 366 | Angel D. Munoz 367 | {version}-beta 368 | {version}-beta 369 | netstandard2.0 370 | true 371 | $(DefineConstants);FABLE_COMPILER; 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | {comps} 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | {'\n'}""" 389 | -------------------------------------------------------------------------------- /src/Generator/Generation.fs: -------------------------------------------------------------------------------- 1 | namespace Sutil.Generator 2 | 3 | open FSharp.Control.Tasks 4 | open System 5 | 6 | open type Text.Encoding 7 | 8 | open System.Runtime.InteropServices 9 | open System.IO 10 | open System.Text.Json 11 | open System.Text.Json.Serialization 12 | open CliWrap 13 | open Types 14 | open System.Threading.Tasks 15 | open Sutil.Generator 16 | 17 | 18 | module Generation = 19 | let private isWindows = 20 | RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 21 | 22 | let private getBytesFromStr (strval: string) = 23 | let b = UTF8.GetBytes(strval) 24 | 25 | ReadOnlySpan b 26 | 27 | let downloadPackage (package: string) = 28 | let cmd = 29 | Cli 30 | .Wrap(if isWindows then "npx.cmd" else "npx") 31 | .WithArguments($"pnpm install {package}") 32 | .WithStandardErrorPipe(PipeTarget.ToStream(System.Console.OpenStandardError())) 33 | .WithStandardOutputPipe(PipeTarget.ToStream(System.Console.OpenStandardOutput())) 34 | 35 | cmd.ExecuteAsync() 36 | 37 | let private getJsonOptions () = 38 | let opts = JsonSerializerOptions() 39 | 40 | opts.AllowTrailingCommas <- true 41 | opts.IgnoreNullValues <- true 42 | opts.ReadCommentHandling <- JsonCommentHandling.Skip 43 | opts.Converters.Add(JsonFSharpConverter()) 44 | opts 45 | 46 | let private parseShoelaceMetadata () = 47 | task { 48 | try 49 | let path = 50 | let combined = 51 | Path.Combine("./", "node_modules", "@shoelace-style", "shoelace", "dist", "metadata.json") 52 | 53 | Path.GetFullPath combined 54 | 55 | use fileStr = File.OpenRead path 56 | 57 | let! serialized = JsonSerializer.DeserializeAsync(fileStr, getJsonOptions ()) 58 | return Some serialized 59 | with ex -> 60 | eprintfn "%s" ex.Message 61 | return None 62 | } 63 | 64 | let private writeShelaceComponentFile (root: string) (comp: SlComponent) = 65 | let name = comp.className.[2..] 66 | let path = Path.Combine(root, $"{name}.fs") 67 | use file = File.Create path 68 | 69 | let bytes = 70 | getBytesFromStr (Shoelace.Templates.getComponentTpl comp) 71 | 72 | file.Write bytes 73 | 74 | let private writeShoelaceLibraryFsProj (root: string) (version: string) (components: SlComponent array) = 75 | let library = Path.Combine(root, "Library.fs") 76 | 77 | let fsproj = 78 | Path.Combine(root, "Sutil.Shoelace.fsproj") 79 | 80 | use library = File.Create library 81 | 82 | let bytes = 83 | getBytesFromStr (Shoelace.Templates.getShoelaceAPIClass components) 84 | 85 | library.Write bytes 86 | use fsproj = File.Create fsproj 87 | 88 | let writeComponents = 89 | Shoelace.Templates.getFsFileReference components 90 | 91 | let bytes = 92 | getBytesFromStr (Shoelace.Templates.getFsProjTpl writeComponents version) 93 | 94 | fsproj.Write bytes 95 | 96 | let private generateShoelaceLib () = 97 | task { 98 | printfn "Generating Shoelace Library..." 99 | printfn "Downloading package @shoelace-style/shoelace" 100 | 101 | let! result = downloadPackage "@shoelace-style/shoelace" 102 | 103 | if result.ExitCode <> 0 then 104 | raise (Exception("Failed to Download the package")) 105 | 106 | let! metadata = parseShoelaceMetadata () 107 | let path = Path.Combine("../", "Sutil.Shoelace") 108 | 109 | let dir = Directory.CreateDirectory(path) 110 | 111 | match metadata with 112 | | Some metadata -> 113 | printfn $"Using Shoelace - {metadata.version} from {metadata.author}, {metadata.license}" 114 | 115 | metadata.components 116 | |> Array.Parallel.iter (writeShelaceComponentFile dir.FullName) 117 | 118 | writeShoelaceLibraryFsProj dir.FullName metadata.version metadata.components 119 | printfn $"Generated {metadata.components.Length} Components" 120 | | None -> 121 | printfn "Failed to parse the metadata.json file, will not continue." 122 | () 123 | } 124 | 125 | 126 | let private writeFastComponentFile (root: string) (comp: TagVsCodeDefinition) = 127 | let name = 128 | let upercased = 129 | comp.name.Split('-') 130 | |> Array.tail 131 | |> Array.map (fun word -> $"{Char.ToUpperInvariant(word.[0])}{word.[1..]}") 132 | 133 | String.Join("", upercased) 134 | 135 | let path = Path.Combine(root, $"{name}.fs") 136 | use file = File.Create path 137 | 138 | let bytes = 139 | getBytesFromStr (Fast.Templates.getComponentTpl comp) 140 | 141 | file.Write bytes 142 | 143 | let private writeFastLibraryFsProj 144 | (root: string) 145 | (package: string) 146 | (version: string) 147 | (components: TagVsCodeDefinition array) 148 | = 149 | let library = Path.Combine(root, "Library.fs") 150 | 151 | let fsproj = Path.Combine(root, "Sutil.Fast.fsproj") 152 | 153 | use library = File.Create library 154 | 155 | let bytes = 156 | getBytesFromStr (Fast.Templates.getFastAPIClass components) 157 | 158 | library.Write bytes 159 | use fsproj = File.Create fsproj 160 | 161 | let writeComponents = 162 | Fast.Templates.getFsFileReference components 163 | 164 | let bytes = 165 | getBytesFromStr (Fast.Templates.getFsProjTpl writeComponents package version) 166 | 167 | fsproj.Write bytes 168 | 169 | let private getFastComponents () = 170 | let tryDeserializeFile (path: string) = 171 | task { 172 | use content = File.OpenRead path 173 | 174 | try 175 | let! definition = 176 | JsonSerializer.DeserializeAsync(content, getJsonOptions ()) 177 | 178 | return definition.tags |> Seq.tryHead 179 | with ex -> 180 | eprintfn "Failed to parse File %s %s" path ex.Message 181 | return None 182 | } 183 | 184 | task { 185 | let path = 186 | let combined = 187 | Path.Combine("./", "node_modules", "@microsoft", "fast-components", "dist", "esm") 188 | 189 | Path.GetFullPath combined 190 | 191 | return! 192 | Directory.GetFiles(path, "*.vscode.definition.json", SearchOption.AllDirectories) 193 | |> Array.Parallel.map tryDeserializeFile 194 | |> Task.WhenAll 195 | } 196 | 197 | let generateFastLib () = 198 | task { 199 | printfn "Generate FAST library..." 200 | printfn "Downloading package @microsoft/fast-components" 201 | 202 | let! result = downloadPackage "@microsoft/fast-components" 203 | 204 | if result.ExitCode <> 0 then 205 | raise (Exception("Failed to Download the package")) 206 | 207 | let! components = getFastComponents () 208 | let result = components |> Array.Parallel.choose id 209 | let path = Path.Combine("../", "Sutil.Fast") 210 | 211 | let dir = Directory.CreateDirectory(path) 212 | let package = Fast.FastPackageJson.GetSample() 213 | let version = package.Version 214 | 215 | printfn $"Using {package.Name} - {version} from {package.Author.Name}, {package.License}" 216 | 217 | result 218 | |> Array.Parallel.iter (writeFastComponentFile dir.FullName) 219 | 220 | writeFastLibraryFsProj dir.FullName package.Name version result 221 | printfn $"Generated {result.Length} Components" 222 | } 223 | 224 | let generateLibrary (componentSystem: ComponentSystem) = 225 | match componentSystem with 226 | | ComponentSystem.Shoelace -> generateShoelaceLib () 227 | | ComponentSystem.Fast -> generateFastLib () 228 | -------------------------------------------------------------------------------- /src/Generator/Generator.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Generator/Program.fs: -------------------------------------------------------------------------------- 1 | // Learn more about F# at http://docs.microsoft.com/dotnet/fsharp 2 | 3 | open System.Threading.Tasks 4 | open FSharp.Control.Tasks 5 | open Sutil.Generator.Generation 6 | open Argu 7 | open Sutil.Generator.Types 8 | 9 | 10 | 11 | [] 12 | let main argv = 13 | 14 | let parser = 15 | ArgumentParser.Create(programName = "Sutil.Generator") 16 | 17 | let result = 18 | parser.Parse(argv, ignoreUnrecognized = true) 19 | 20 | let activeTask () = 21 | match result.GetAllResults() with 22 | | [ Component_System ComponentSystem.Fast ] -> 23 | task { 24 | do! generateLibrary ComponentSystem.Fast 25 | return 0 26 | } 27 | | [ Component_System ComponentSystem.Shoelace ] 28 | | _ -> 29 | task { 30 | do! generateLibrary ComponentSystem.Shoelace 31 | return 0 32 | } 33 | 34 | activeTask () 35 | |> Async.AwaitTask 36 | |> Async.RunSynchronously 37 | -------------------------------------------------------------------------------- /src/Generator/Shoelace.fs: -------------------------------------------------------------------------------- 1 | namespace Sutil.Generator.Shoelace 2 | open Sutil.Generator.Types 3 | 4 | (* 5 | DO NOT USE FANTOMAS OR ANOTHER FORMATTER 6 | THAT CHANGES THE MULTI-LINE STRING STRUCTURE ON THIS FILE 7 | IT WILL BREAK THE DOC COMMENTS GENERATION 8 | *) 9 | 10 | [] 11 | module Templates = 12 | 13 | open System.Web 14 | 15 | 16 | let getPropName (name: string) = 17 | match name with 18 | | "type" -> "``type``" 19 | | "open" -> "``open``" 20 | | "inline" -> "``inline``" 21 | | "checked" -> "``checked``" 22 | | rest -> rest 23 | 24 | let getAttrName (name: string) = 25 | match name with 26 | | "type" -> "type'" 27 | | "open" -> "open'" 28 | | "inline" -> "inline'" 29 | | "checked" -> "checked'" 30 | | name -> name 31 | 32 | let getTypeType (type': string) = 33 | match type' with 34 | | "boolean" -> "bool" 35 | | "void" -> "unit" 36 | | "number" -> "float" 37 | | "" -> "string array" 38 | | "PlaybackDirection" -> "string" 39 | | "FillMode" -> "string" 40 | | "FocusOptions" -> "{| preventScroll: bool option |}" 41 | | type' when type'.StartsWith('{') || type'.EndsWith('}') -> "obj" 42 | | type' when type'.Contains('|') -> "string" 43 | | type' -> type'.Replace("'", "") 44 | 45 | let getDefaultValue (defaultValue: string) = 46 | match defaultValue with 47 | | "..." -> "None" 48 | | "undefined" 49 | | "null" 50 | | null -> "None" 51 | | value -> $"""Some {value.Replace("'", "\"")}""" 52 | 53 | let getSlPropTpl (prop: SlProp) = 54 | let name = getPropName prop.name 55 | let type' = getTypeType prop.``type`` 56 | 57 | let description = 58 | HttpUtility.HtmlEncode(prop.description.Replace("\n", "\n /// ")) 59 | 60 | $""" /// {description} 61 | abstract member {name} : {type'} with get, set""" 62 | 63 | 64 | let private getEventList (events: SlEvents array) (padding: int option) = 65 | let padding = defaultArg padding 0 66 | let padding = " " |> String.replicate padding 67 | 68 | let getEvents = 69 | events 70 | |> Array.fold 71 | (fun (current: string) next -> 72 | let description = 73 | HttpUtility.HtmlEncode(next.description.Replace("\n", "\n /// ")) 74 | 75 | let eventDetails = 76 | match next.details with 77 | | "void" -> "unit" 78 | | rest -> rest 79 | 80 | let current = 81 | if current.Length > 0 then 82 | $"{current}\n" 83 | else 84 | "" 85 | 86 | $"{padding}{current}{padding}/// - `{next.name}`: {description}") 87 | "" 88 | 89 | $"""{padding}/// Events: 90 | {padding}{getEvents.Trim()}""" 91 | 92 | let private getSlotList (slots: SlSlots array) (padding: int option) = 93 | let padding = defaultArg padding 0 94 | let padding = " " |> String.replicate padding 95 | 96 | let getSlots = 97 | slots 98 | |> Array.fold 99 | (fun (current: string) next -> 100 | let description = 101 | HttpUtility.HtmlEncode(next.description.Replace("\n", "\n /// ")) 102 | 103 | let name = 104 | if next.name.Length = 0 then 105 | "default" 106 | else 107 | next.name 108 | 109 | let current = 110 | if current.Length > 0 then 111 | $"{current}\n" 112 | else 113 | "" 114 | 115 | $"{padding}{current}{padding}/// - `{name}`: {description}") 116 | "" 117 | 118 | $"""{padding}/// Slots: 119 | {padding}{getSlots.Trim()}""" 120 | 121 | 122 | let getComponentCommentTpl (comp: SlComponent) (padding: int option) = 123 | let spacePadding = defaultArg padding 0 124 | let padding = " " |> String.replicate spacePadding 125 | 126 | $""" 127 | {padding}/// 128 | {padding}/// Tag: {comp.tag} 129 | {padding}/// 130 | {padding}/// Since: {comp.since} 131 | {padding}/// 132 | {padding}/// Status: {comp.status}. 133 | {padding}/// 134 | {padding}/// File: {comp.file} 135 | {padding}/// 136 | {getEventList comp.events (Some spacePadding)} 137 | {padding}/// 138 | {getSlotList comp.slots (Some spacePadding)} 139 | {padding}/// 140 | {padding}/// """ 141 | 142 | 143 | let slMethodTpl (method: SlMethod) = 144 | let name = method.name 145 | 146 | let description = 147 | HttpUtility.HtmlEncode(method.description.Replace("\n", "\n /// ")) 148 | 149 | let prams = 150 | method.``params`` 151 | |> Array.fold 152 | (fun (current: string) (next: {| isOptional: option 153 | name: string 154 | ``type``: string |}) -> 155 | let current = 156 | if current.Length > 0 then 157 | $"{current}" 158 | else 159 | "" 160 | 161 | let type' = getTypeType next.``type`` 162 | 163 | let isOption = 164 | match next.isOptional |> Option.defaultValue false with 165 | | true -> " option" 166 | | false -> "" 167 | 168 | 169 | $"{current} {type'}{isOption} ->") 170 | "" 171 | 172 | $""" /// {description} 173 | abstract member {name} : {prams} unit""" 174 | 175 | let slPropAttrTpl (prop: SlProp) = 176 | let name = getAttrName prop.name 177 | let type' = getTypeType prop.``type`` 178 | 179 | let description = 180 | HttpUtility.HtmlEncode(prop.description.Replace("\n", "\n /// ")) 181 | 182 | $""" /// {description} 183 | {name} : {type'} option""" 184 | 185 | 186 | let private getProps (props: SlProp array) = 187 | props 188 | |> Array.fold (fun (current: string) (next: SlProp) -> $"{current}\n{getSlPropTpl next}") "" 189 | 190 | let private getAttrs (props: SlProp array) = 191 | props 192 | |> Array.fold (fun (current: string) (next: SlProp) -> $"{current}\n{slPropAttrTpl next}") "" 193 | 194 | let private getMethods (methods: SlMethod array) = 195 | methods 196 | |> Array.fold (fun (current: string) (next: SlMethod) -> $"{current}\n{slMethodTpl next}") "" 197 | 198 | let private getAttrRecordMemberValue (prop: SlProp) = 199 | let name = getAttrName prop.name 200 | let type' = getTypeType prop.``type`` 201 | let value = getDefaultValue prop.defaultValue 202 | 203 | let addDot = 204 | if type' = "float" 205 | && value <> "None" 206 | && value <> "Some Fable.Core.JS.Infinity" then 207 | "." 208 | else 209 | "" 210 | 211 | $"{name} = {value}{addDot}" 212 | 213 | 214 | let getWithFunction (prop: SlProp) (attrsName: string) = 215 | let name = prop.name 216 | let propAttr = getAttrName prop.name 217 | let type' = getTypeType prop.``type`` 218 | 219 | let valuesComment = 220 | let values = 221 | (prop.values |> Option.defaultValue [||]) 222 | 223 | let description = 224 | HttpUtility.HtmlEncode(prop.description.Replace("\n", "\n /// ")) 225 | 226 | $"\n /// {description}\n /// Default Value: {prop.defaultValue}\n /// Type: {prop.``type``}" 227 | 228 | $"""{valuesComment} 229 | let with{name.[0] |> System.Char.ToUpper}{name.[1..]} ({propAttr}: {type'}) (attrs: {attrsName}) = 230 | {{ attrs with {propAttr} = Some {propAttr} }}""" 231 | 232 | let getAttrModule (className: string) (comps: SlProp array) = 233 | let getAttrs = 234 | comps 235 | |> Array.fold (fun current next -> $"{current}\n {getAttrRecordMemberValue next}") "" 236 | 237 | let withFunctions = 238 | let attrName = $"{className}Attributes" 239 | 240 | comps 241 | |> Array.fold (fun current next -> $"{current}\n {getWithFunction next attrName}") "" 242 | 243 | $""" 244 | /// 245 | [] 246 | module {className}Attributes = 247 | let create(): {className}Attributes = {{ {getAttrs} 248 | }} 249 | {withFunctions}""" 250 | 251 | let getCompModule (tagAndName: string * string) (comp: SlProp array) = 252 | let (tag, className) = tagAndName 253 | 254 | let getBindingsTpl = 255 | let props = 256 | comp 257 | |> Array.fold 258 | (fun current next -> 259 | let name = getAttrName next.name 260 | $"{current}\n let {name} = attrs .> (fun attrs -> attrs.{name})") 261 | "" 262 | 263 | let bindings = 264 | comp 265 | |> Array.fold 266 | (fun (current: string) next -> 267 | let name = getAttrName next.name 268 | 269 | let tag = 270 | next.attribute |> Option.defaultValue next.name 271 | 272 | let current = 273 | if current.Length > 0 then 274 | $"{current}\n " 275 | else 276 | "" 277 | 278 | $"{current}Bind.attr(\"{tag}\", {name})") 279 | "" 280 | 281 | if props.Length > 0 then 282 | $""" 283 | /// Provides all of the bindings available for the HTML element 284 | /// It leverages "Bind.attr("attribute-name", observable)" to provide reactive changes 285 | let stateful (attrs: IStore<{className}Attributes>) (nodes: NodeFactory seq) = 286 | {props} 287 | stateless 288 | [ {bindings} 289 | yield! nodes ]""" 290 | else 291 | "" 292 | 293 | $""" 294 | /// 295 | [] 296 | module {className} = 297 | /// doesn't provide any binding helper logic and allows the user to take full 298 | /// control over the HTML Element either to create static HTML or do custom bindings 299 | /// via "bindFragment" or "Bind.attr("", binding)" 300 | let stateless (content: NodeFactory seq) = Html.custom("{tag}", content) 301 | {getBindingsTpl}""" 302 | 303 | let getComponentTpl (comp: SlComponent) = 304 | let moduleName = comp.className.[2..] 305 | let props = getProps comp.props 306 | let attrs = getAttrs comp.props 307 | let methods = getMethods comp.methods 308 | 309 | let attrsTpl = 310 | if comp.props.Length > 0 then 311 | $"\n///\ntype {comp.className}Attributes = {{ {attrs}\n}}" 312 | else 313 | "" 314 | 315 | let attrsModule = 316 | if comp.props.Length > 0 then 317 | getAttrModule comp.className comp.props 318 | else 319 | "" 320 | 321 | $""" 322 | module Sutil.Shoelace.{moduleName} 323 | open Browser.Types 324 | open Sutil 325 | open Sutil.DOM{getComponentCommentTpl comp None} 326 | [] 327 | type {comp.className} = 328 | inherit HTMLElement 329 | {props} 330 | {methods} 331 | {attrsTpl}{attrsModule}{getCompModule (comp.tag, comp.className) (comp.props)}""" 332 | 333 | 334 | let getStatefulComponentTpl (comp: SlComponent) = 335 | if comp.props.Length > 0 then 336 | $"""{getComponentCommentTpl comp (Some 4)} 337 | static member inline {comp.className} (attrs: IStore<{comp.className}Attributes>, content: NodeFactory seq) = 338 | {comp.className}.stateful attrs content""" 339 | else 340 | "" 341 | 342 | let getStatelessComponentTpl (comp: SlComponent) = 343 | $"""{getComponentCommentTpl comp (Some 4)} 344 | static member inline {comp.className} (content: NodeFactory seq) = 345 | {comp.className}.stateless content""" 346 | 347 | let getShoelaceAPIClass (components: SlComponent array) = 348 | let opens = 349 | components 350 | |> Array.fold 351 | (fun (current: string) next -> 352 | let name = next.className.[2..] 353 | 354 | let current = 355 | if current.Length > 0 then 356 | $"{current}\n" 357 | else 358 | "" 359 | 360 | $"{current}open Sutil.Shoelace.{name}") 361 | "" 362 | 363 | let methods = 364 | components 365 | |> Array.fold 366 | (fun (current: string) next -> 367 | let current = 368 | if current.Length > 0 then 369 | $"{current}\n " 370 | else 371 | "" 372 | 373 | let stateful = getStatefulComponentTpl next 374 | 375 | let stateless = getStatelessComponentTpl next 376 | 377 | $"{current}{stateless}{stateful}") 378 | "" 379 | 380 | $"""namespace Sutil.Shoelace 381 | open Sutil 382 | open Sutil.DOM 383 | {opens} 384 | /// 385 | /// Provides a simple API to access all Shoelace Components available 386 | /// 387 | type Shoelace = 388 | {methods}""" 389 | 390 | 391 | let getFsFileReference (components: SlComponent array) = 392 | components 393 | |> Array.fold 394 | (fun (current: string) (next: SlComponent) -> 395 | $"{current}\n ") 396 | """""" 397 | 398 | let getFsProjTpl (comps: string) (version: string) = 399 | $""" 400 | 401 | 402 | Sutil bindings for Shoelace Web Components. 403 | The contents of this package are auto-generated 404 | 405 | https://github.com/AngelMunoz/Sutil.Generator 406 | https://github.com/AngelMunoz/Sutil.Generator 407 | 408 | fsharp;fable;svelte 409 | Angel D. Munoz 410 | {version} 411 | {version} 412 | netstandard2.0 413 | true 414 | $(DefineConstants);FABLE_COMPILER; 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | {comps} 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | {'\n'}""" 431 | -------------------------------------------------------------------------------- /src/Generator/Types.fs: -------------------------------------------------------------------------------- 1 | namespace Sutil.Generator 2 | 3 | open Argu 4 | 5 | module Types = 6 | 7 | [] 8 | type ComponentSystem = 9 | | Fast 10 | | Shoelace 11 | 12 | type Args = 13 | | [] Component_System of ComponentSystem 14 | interface IArgParserTemplate with 15 | member this.Usage = 16 | match this with 17 | | Component_System _ -> "You need to specify the component System" 18 | 19 | type SlProp = 20 | { name: string 21 | description: string 22 | ``type``: string 23 | attribute: string option 24 | defaultValue: string 25 | values: string array option } 26 | 27 | type SlMethod = 28 | { name: string 29 | description: string 30 | ``params``: {| name: string 31 | ``type``: string 32 | isOptional: bool option |} array } 33 | 34 | type SlEvents = 35 | { name: string 36 | description: string 37 | details: string } 38 | 39 | type SlSlots = { name: string; description: string } 40 | type SlCssCustomProperties = { name: string; description: string } 41 | type SlParts = { name: string; description: string } 42 | type SlAnimation = { name: string; description: string } 43 | 44 | type SlComponent = 45 | { className: string 46 | tag: string 47 | file: string 48 | since: string 49 | status: string 50 | props: SlProp array 51 | methods: SlMethod array 52 | events: SlEvents array 53 | slots: SlSlots array 54 | cssCustomProperties: SlCssCustomProperties array 55 | parts: SlParts array 56 | dependencies: string array 57 | animations: SlAnimation array } 58 | 59 | type ShoelaceMetadata = 60 | { name: string 61 | description: string 62 | version: string 63 | author: string 64 | homepage: string 65 | license: string 66 | components: SlComponent array } 67 | 68 | type AttributeVscodeDefinition = 69 | { name: string 70 | title: string 71 | ``type``: string 72 | description: string option 73 | ``default``: obj option 74 | required: bool option 75 | values: seq<{| name: string |}> option 76 | value: obj option } 77 | 78 | type SlotVsCodeDefinition = 79 | { name: string 80 | title: string 81 | description: string option } 82 | 83 | type TagVsCodeDefinition = 84 | { name: string 85 | title: string 86 | description: string 87 | attributes: seq 88 | slots: seq } 89 | 90 | 91 | type HtmlCustomDataVSC = 92 | { version: float 93 | tags: seq } 94 | -------------------------------------------------------------------------------- /src/Generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "@microsoft/fast-components": "1.21.6", 5 | "@shoelace-style/shoelace": "2.0.0-beta.43" 6 | }, 7 | "peerDependencies": { 8 | "lodash-es": "4.17.21" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Generator/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.3 2 | 3 | specifiers: 4 | '@microsoft/fast-components': 1.21.6 5 | '@shoelace-style/shoelace': 2.0.0-beta.43 6 | 7 | dependencies: 8 | '@microsoft/fast-components': 1.21.6 9 | '@shoelace-style/shoelace': 2.0.0-beta.43 10 | 11 | packages: 12 | 13 | /@lit/reactive-element/1.0.0-rc.2: 14 | resolution: {integrity: sha512-cujeIl5Ei8FC7UHf4/4Q3bRJOtdTe1vpJV/JEBYCggedmQ+2P8A2oz7eE+Vxi6OJ4nc0X+KZxXnBoH4QrEbmEQ==} 15 | dev: false 16 | 17 | /@microsoft/fast-colors/5.1.3: 18 | resolution: {integrity: sha512-XDEnRYxPO5P3Jsizm4TCxLu1osS/uV3Lym6SfRhq2PxfXPTgEcdvOYDUXyV2drqebs3U5VQnOcYcJiSp73xhng==} 19 | dev: false 20 | 21 | /@microsoft/fast-components/1.21.6: 22 | resolution: {integrity: sha512-z7/kaeEehmCiwFoCNKsXeEZNZNe595HVRmLG6SIOkbkdHMFaw/0Gq2M8j8kkPq+cL+ZDxuSmiCdMsaqSgbNGhA==} 23 | peerDependencies: 24 | lodash-es: ^4.0.0 25 | dependencies: 26 | '@microsoft/fast-colors': 5.1.3 27 | '@microsoft/fast-element': 1.4.0 28 | '@microsoft/fast-foundation': 1.24.6 29 | '@microsoft/fast-web-utilities': 4.8.0 30 | tslib: 1.14.1 31 | vscode-html-languageservice: 4.0.5 32 | dev: false 33 | 34 | /@microsoft/fast-element/1.4.0: 35 | resolution: {integrity: sha512-7BC/juFc7S4HMGt/tNCP9bjwtUslheGiPM2jtIibe1bj+PO34woUfH5TxaklOT6sRStig/0fODX4R5oqY4DYLA==} 36 | dev: false 37 | 38 | /@microsoft/fast-foundation/1.24.6: 39 | resolution: {integrity: sha512-uZH/E+4hrwc0/rcOUg+JzqzxS4HiQidgGCFQwWQIh0sNduveoaKDKIHD4STokmkWhr02TTDyqizrFD0hPmCHNA==} 40 | dependencies: 41 | '@microsoft/fast-element': 1.4.0 42 | '@microsoft/fast-web-utilities': 4.8.0 43 | '@microsoft/tsdoc-config': 0.13.9 44 | tabbable: 5.2.0 45 | tslib: 1.14.1 46 | transitivePeerDependencies: 47 | - lodash-es 48 | dev: false 49 | 50 | /@microsoft/fast-web-utilities/4.8.0: 51 | resolution: {integrity: sha512-+MroMIP5yGD8mqbegqSZoIbQVjvmsQRQtn87Gc8TJk00KIfRu2x9sFAq8q5m8H61sjCRHreJ0Bq5telz09h55g==} 52 | peerDependencies: 53 | lodash-es: ^4.17.10 54 | dependencies: 55 | exenv-es6: 1.0.0 56 | dev: false 57 | 58 | /@microsoft/tsdoc-config/0.13.9: 59 | resolution: {integrity: sha512-VqqZn+rT9f6XujFPFR2aN9XKF/fuir/IzKVzoxI0vXIzxysp4ee6S2jCakmlGFHEasibifFTsJr7IYmRPxfzYw==} 60 | dependencies: 61 | '@microsoft/tsdoc': 0.12.24 62 | ajv: 6.12.6 63 | jju: 1.4.0 64 | resolve: 1.19.0 65 | dev: false 66 | 67 | /@microsoft/tsdoc/0.12.24: 68 | resolution: {integrity: sha512-Mfmij13RUTmHEMi9vRUhMXD7rnGR2VvxeNYtaGtaJ4redwwjT4UXYJ+nzmVJF7hhd4pn/Fx5sncDKxMVFJSWPg==} 69 | dev: false 70 | 71 | /@nodelib/fs.scandir/2.1.5: 72 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 73 | engines: {node: '>= 8'} 74 | dependencies: 75 | '@nodelib/fs.stat': 2.0.5 76 | run-parallel: 1.2.0 77 | dev: false 78 | 79 | /@nodelib/fs.stat/2.0.5: 80 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 81 | engines: {node: '>= 8'} 82 | dev: false 83 | 84 | /@nodelib/fs.walk/1.2.7: 85 | resolution: {integrity: sha512-BTIhocbPBSrRmHxOAJFtR18oLhxTtAFDAvL8hY1S3iU8k+E60W/YFs4jrixGzQjMpF4qPXxIQHcjVD9dz1C2QA==} 86 | engines: {node: '>= 8'} 87 | dependencies: 88 | '@nodelib/fs.scandir': 2.1.5 89 | fastq: 1.11.0 90 | dev: false 91 | 92 | /@popperjs/core/2.9.2: 93 | resolution: {integrity: sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==} 94 | dev: false 95 | 96 | /@shoelace-style/animations/1.1.0: 97 | resolution: {integrity: sha512-Be+cahtZyI2dPKRm8EZSx3YJQ+jLvEcn3xzRP7tM4tqBnvd/eW/64Xh0iOf0t2w5P8iJKfdBbpVNE9naCaOf2g==} 98 | dev: false 99 | 100 | /@shoelace-style/shoelace/2.0.0-beta.43: 101 | resolution: {integrity: sha512-7CAE5umw7pr3QiYyQmp5dcKnBazV9gcP4TtEUyKUTHLi2Bag3FTOyedXTjFB3XpfRmfwL5Y1lhiQu7cje7BBlw==} 102 | dependencies: 103 | '@popperjs/core': 2.9.2 104 | '@shoelace-style/animations': 1.1.0 105 | color: 3.1.3 106 | globby: 11.0.4 107 | lit: 2.0.0-rc.2 108 | lit-html: 2.0.0-rc.3 109 | qr-creator: 1.0.0 110 | dev: false 111 | 112 | /@types/trusted-types/1.0.6: 113 | resolution: {integrity: sha512-230RC8sFeHoT6sSUlRO6a8cAnclO06eeiq1QDfiv2FGCLWFvvERWgwIQD4FWqD9A69BN7Lzee4OXwoMVnnsWDw==} 114 | dev: false 115 | 116 | /ajv/6.12.6: 117 | resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 118 | dependencies: 119 | fast-deep-equal: 3.1.3 120 | fast-json-stable-stringify: 2.1.0 121 | json-schema-traverse: 0.4.1 122 | uri-js: 4.4.1 123 | dev: false 124 | 125 | /array-union/2.1.0: 126 | resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 127 | engines: {node: '>=8'} 128 | dev: false 129 | 130 | /braces/3.0.2: 131 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 132 | engines: {node: '>=8'} 133 | dependencies: 134 | fill-range: 7.0.1 135 | dev: false 136 | 137 | /color-convert/1.9.3: 138 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 139 | dependencies: 140 | color-name: 1.1.3 141 | dev: false 142 | 143 | /color-name/1.1.3: 144 | resolution: {integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=} 145 | dev: false 146 | 147 | /color-name/1.1.4: 148 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 149 | dev: false 150 | 151 | /color-string/1.5.5: 152 | resolution: {integrity: sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==} 153 | dependencies: 154 | color-name: 1.1.4 155 | simple-swizzle: 0.2.2 156 | dev: false 157 | 158 | /color/3.1.3: 159 | resolution: {integrity: sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==} 160 | dependencies: 161 | color-convert: 1.9.3 162 | color-string: 1.5.5 163 | dev: false 164 | 165 | /dir-glob/3.0.1: 166 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 167 | engines: {node: '>=8'} 168 | dependencies: 169 | path-type: 4.0.0 170 | dev: false 171 | 172 | /exenv-es6/1.0.0: 173 | resolution: {integrity: sha512-fcG/TX8Ruv9Ma6PBaiNsUrHRJzVzuFMP6LtPn/9iqR+nr9mcLeEOGzXQGLC5CVQSXGE98HtzW2mTZkrCA3XrDg==} 174 | dev: false 175 | 176 | /fast-deep-equal/3.1.3: 177 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 178 | dev: false 179 | 180 | /fast-glob/3.2.5: 181 | resolution: {integrity: sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==} 182 | engines: {node: '>=8'} 183 | dependencies: 184 | '@nodelib/fs.stat': 2.0.5 185 | '@nodelib/fs.walk': 1.2.7 186 | glob-parent: 5.1.2 187 | merge2: 1.4.1 188 | micromatch: 4.0.4 189 | picomatch: 2.3.0 190 | dev: false 191 | 192 | /fast-json-stable-stringify/2.1.0: 193 | resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 194 | dev: false 195 | 196 | /fastq/1.11.0: 197 | resolution: {integrity: sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==} 198 | dependencies: 199 | reusify: 1.0.4 200 | dev: false 201 | 202 | /fill-range/7.0.1: 203 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 204 | engines: {node: '>=8'} 205 | dependencies: 206 | to-regex-range: 5.0.1 207 | dev: false 208 | 209 | /function-bind/1.1.1: 210 | resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} 211 | dev: false 212 | 213 | /glob-parent/5.1.2: 214 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 215 | engines: {node: '>= 6'} 216 | dependencies: 217 | is-glob: 4.0.1 218 | dev: false 219 | 220 | /globby/11.0.4: 221 | resolution: {integrity: sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==} 222 | engines: {node: '>=10'} 223 | dependencies: 224 | array-union: 2.1.0 225 | dir-glob: 3.0.1 226 | fast-glob: 3.2.5 227 | ignore: 5.1.8 228 | merge2: 1.4.1 229 | slash: 3.0.0 230 | dev: false 231 | 232 | /has/1.0.3: 233 | resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} 234 | engines: {node: '>= 0.4.0'} 235 | dependencies: 236 | function-bind: 1.1.1 237 | dev: false 238 | 239 | /ignore/5.1.8: 240 | resolution: {integrity: sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==} 241 | engines: {node: '>= 4'} 242 | dev: false 243 | 244 | /is-arrayish/0.3.2: 245 | resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} 246 | dev: false 247 | 248 | /is-core-module/2.4.0: 249 | resolution: {integrity: sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==} 250 | dependencies: 251 | has: 1.0.3 252 | dev: false 253 | 254 | /is-extglob/2.1.1: 255 | resolution: {integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=} 256 | engines: {node: '>=0.10.0'} 257 | dev: false 258 | 259 | /is-glob/4.0.1: 260 | resolution: {integrity: sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==} 261 | engines: {node: '>=0.10.0'} 262 | dependencies: 263 | is-extglob: 2.1.1 264 | dev: false 265 | 266 | /is-number/7.0.0: 267 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 268 | engines: {node: '>=0.12.0'} 269 | dev: false 270 | 271 | /jju/1.4.0: 272 | resolution: {integrity: sha1-o6vicYryQaKykE+EpiWXDzia4yo=} 273 | dev: false 274 | 275 | /json-schema-traverse/0.4.1: 276 | resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 277 | dev: false 278 | 279 | /lit-element/3.0.0-rc.2: 280 | resolution: {integrity: sha512-2Z7DabJ3b5K+p5073vFjMODoaWqy5PIaI4y6ADKm+fCGc8OnX9fU9dMoUEBZjFpd/bEFR9PBp050tUtBnT9XTQ==} 281 | dependencies: 282 | '@lit/reactive-element': 1.0.0-rc.2 283 | lit-html: 2.0.0-rc.3 284 | dev: false 285 | 286 | /lit-html/2.0.0-rc.3: 287 | resolution: {integrity: sha512-Y6P8LlAyQuqvzq6l/Nc4z5/P5M/rVLYKQIRxcNwSuGajK0g4kbcBFQqZmgvqKG+ak+dHZjfm2HUw9TF5N/pkCw==} 288 | dependencies: 289 | '@types/trusted-types': 1.0.6 290 | dev: false 291 | 292 | /lit/2.0.0-rc.2: 293 | resolution: {integrity: sha512-BOCuoJR04WaTV8UqTKk09cNcQA10Aq2LCcBOiHuF7TzWH5RNDsbCBP5QM9sLBSotGTXbDug/gFO08jq6TbyEtw==} 294 | dependencies: 295 | '@lit/reactive-element': 1.0.0-rc.2 296 | lit-element: 3.0.0-rc.2 297 | lit-html: 2.0.0-rc.3 298 | dev: false 299 | 300 | /merge2/1.4.1: 301 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 302 | engines: {node: '>= 8'} 303 | dev: false 304 | 305 | /micromatch/4.0.4: 306 | resolution: {integrity: sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==} 307 | engines: {node: '>=8.6'} 308 | dependencies: 309 | braces: 3.0.2 310 | picomatch: 2.3.0 311 | dev: false 312 | 313 | /path-parse/1.0.7: 314 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 315 | dev: false 316 | 317 | /path-type/4.0.0: 318 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 319 | engines: {node: '>=8'} 320 | dev: false 321 | 322 | /picomatch/2.3.0: 323 | resolution: {integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==} 324 | engines: {node: '>=8.6'} 325 | dev: false 326 | 327 | /punycode/2.1.1: 328 | resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} 329 | engines: {node: '>=6'} 330 | dev: false 331 | 332 | /qr-creator/1.0.0: 333 | resolution: {integrity: sha512-C0cqfbS1P5hfqN4NhsYsUXePlk9BO+a45bAQ3xLYjBL3bOIFzoVEjs79Fado9u9BPBD3buHi3+vY+C8tHh4qMQ==} 334 | dev: false 335 | 336 | /queue-microtask/1.2.3: 337 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 338 | dev: false 339 | 340 | /resolve/1.19.0: 341 | resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} 342 | dependencies: 343 | is-core-module: 2.4.0 344 | path-parse: 1.0.7 345 | dev: false 346 | 347 | /reusify/1.0.4: 348 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 349 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 350 | dev: false 351 | 352 | /run-parallel/1.2.0: 353 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 354 | dependencies: 355 | queue-microtask: 1.2.3 356 | dev: false 357 | 358 | /simple-swizzle/0.2.2: 359 | resolution: {integrity: sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=} 360 | dependencies: 361 | is-arrayish: 0.3.2 362 | dev: false 363 | 364 | /slash/3.0.0: 365 | resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 366 | engines: {node: '>=8'} 367 | dev: false 368 | 369 | /tabbable/5.2.0: 370 | resolution: {integrity: sha512-0uyt8wbP0P3T4rrsfYg/5Rg3cIJ8Shl1RJ54QMqYxm1TLdWqJD1u6+RQjr2Lor3wmfT7JRHkirIwy99ydBsyPg==} 371 | dev: false 372 | 373 | /to-regex-range/5.0.1: 374 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 375 | engines: {node: '>=8.0'} 376 | dependencies: 377 | is-number: 7.0.0 378 | dev: false 379 | 380 | /tslib/1.14.1: 381 | resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} 382 | dev: false 383 | 384 | /uri-js/4.4.1: 385 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 386 | dependencies: 387 | punycode: 2.1.1 388 | dev: false 389 | 390 | /vscode-html-languageservice/4.0.5: 391 | resolution: {integrity: sha512-9ZKp7nfR6ObUA+K65GfgDPdOmXaPH8MOWxE2RwWF3tVnVMq2w+COKjDNHMvv+uNxtmaRT7/skls7CD/HzrW99w==} 392 | dependencies: 393 | vscode-languageserver-textdocument: 1.0.1 394 | vscode-languageserver-types: 3.16.0 395 | vscode-nls: 5.0.0 396 | vscode-uri: 3.0.2 397 | dev: false 398 | 399 | /vscode-languageserver-textdocument/1.0.1: 400 | resolution: {integrity: sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA==} 401 | dev: false 402 | 403 | /vscode-languageserver-types/3.16.0: 404 | resolution: {integrity: sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==} 405 | dev: false 406 | 407 | /vscode-nls/5.0.0: 408 | resolution: {integrity: sha512-u0Lw+IYlgbEJFF6/qAqG2d1jQmJl0eyAGJHoAJqr2HT4M2BNuQYSEiSE75f52pXHSJm8AlTjnLLbBFPrdz2hpA==} 409 | dev: false 410 | 411 | /vscode-uri/3.0.2: 412 | resolution: {integrity: sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA==} 413 | dev: false 414 | -------------------------------------------------------------------------------- /src/website/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.fs] 2 | indent_size=2 3 | max_line_length=80 4 | fsharp_single_argument_web_mode=true -------------------------------------------------------------------------------- /src/website/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "sutil-shoelace" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/website/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | [Bb]in/ 4 | [Oo]bj/ 5 | 6 | package-lock.json 7 | bundle.js 8 | *.fs.js 9 | .fake 10 | .ionide 11 | 12 | dist/ 13 | .cache/ -------------------------------------------------------------------------------- /src/website/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Sutil Elmish 2 | 3 | ## Changelog (most recent first) 4 | 5 | - Updated to Sutil 1.0.0-alpha-004 6 | - Converted to Sutil 0.2-*-* (latest Feliz.Engine) 7 | - Remove transitive dependencies from fsproj 8 | - Remove use of .> operator 9 | - Rename counter to getCounter 10 | - Formatting -------------------------------------------------------------------------------- /src/website/README.md: -------------------------------------------------------------------------------- 1 | ## Sutil Template for Elmish 2 | 3 | This is a Sutil (**Svelte**) application template which kind of shows a bit how you can* structure sutil applications and work with stores 4 | 5 | ### Quick Start 6 | 7 | ``` 8 | dotnet tool restore 9 | pnpm install # or npm install or yarn install 10 | pnpm start # or npm run start or yarn run start 11 | ``` 12 | 13 | > \* this is not a strict way to do it it's just *A Way* to do it so feel free to remove/add whatever you need in your day to day 14 | -------------------------------------------------------------------------------- /src/website/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/website/markdown.pl.js: -------------------------------------------------------------------------------- 1 | const hljs = require('highlight.js'); 2 | const fs = require('fs/promises'); 3 | 4 | /** 5 | * 6 | * @param {import('snowpack').SnowpackConfig} snowpackConfig 7 | * @param {Record | undefined | null} pluginOptions 8 | * @returns {import('snowpack').SnowpackPlugin} 9 | */ 10 | function markdownPlugin(snowpackConfig, pluginOptions) { 11 | const Md = require('markdown-it')({ 12 | breaks: true, 13 | typographer: true, 14 | linkify: true, 15 | html: true, 16 | highlight(str, lang) { 17 | if (lang && hljs.getLanguage(lang)) { 18 | try { 19 | return hljs.highlight(str, { language: lang }).value; 20 | } catch (__) { } 21 | } 22 | return ''; // use external default escaping 23 | }, 24 | ...(pluginOptions && pluginOptions) 25 | }); 26 | 27 | return { 28 | resolve: { 29 | input: [".md"], 30 | output: [".html"] 31 | }, 32 | async load({ filePath }) { 33 | const content = await fs.readFile(filePath, { encoding: 'utf8' }); 34 | return Md.render(content); 35 | } 36 | }; 37 | 38 | } 39 | 40 | 41 | module.exports = markdownPlugin; -------------------------------------------------------------------------------- /src/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "prestart": "dotnet tool restore", 5 | "start": "dotnet fable watch src --run snowpack dev", 6 | "build": "pnpm prestart && dotnet fable src --run snowpack build", 7 | "deploy": "pnpm build && firebase deploy" 8 | }, 9 | "devDependencies": { 10 | "@snowpack/plugin-dotenv": "~2.1.0", 11 | "firebase-tools": "^9.12.1", 12 | "markdown-it": "~12.0.6", 13 | "rollup": "~2.50.2", 14 | "snowpack": "~3.5.1" 15 | }, 16 | "dependencies": { 17 | "@microsoft/fast-components": "^1.21.6", 18 | "@microsoft/fast-foundation": "1.24.6", 19 | "@shoelace-style/shoelace": "2.0.0-beta.43", 20 | "firacode": "^5.2.0", 21 | "highlight.js": "~11.0.1", 22 | "lodash-es": "4.17.21", 23 | "navigo": "~8.11.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/website/public/fable.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AngelMunoz/Sutil.Generator/ff146e00929ba371cb36740e51418538c79d01d7/src/website/public/fable.ico -------------------------------------------------------------------------------- /src/website/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sutil.Shoelace 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/website/snowpack.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("snowpack").SnowpackUserConfig } */ 2 | module.exports = { 3 | 4 | mount: { 5 | public: { url: '/', static: true }, 6 | src: { url: '/dist' }, 7 | 'node_modules/@shoelace-style/shoelace/dist/assets': { url: '/shoelace/assets', static: true }, 8 | 'node_modules/firacode/distr/woff': { url: '/woff', static: true }, 9 | 'node_modules/firacode/distr/woff2': { url: '/woff2', static: true }, 10 | '../Sutil.Shoelace': { url: '/Sutil.Shoelace', static: true }, 11 | '../Sutil.Fast': { url: '/Sutil.Fast', static: true }, 12 | }, 13 | plugins: [ 14 | '@snowpack/plugin-dotenv', 15 | './markdown.pl.js' 16 | ], 17 | routes: [], 18 | optimize: { 19 | /* Example: Bundle your final build: */ 20 | bundle: true, 21 | splitting: true, 22 | treeshake: true, 23 | manifest: true, 24 | target: 'es2017', 25 | minify: true 26 | }, 27 | packageOptions: { 28 | /* ... */ 29 | polyfillNode: true 30 | }, 31 | devOptions: { 32 | /* ... */ 33 | }, 34 | buildOptions: { 35 | /* ... */ 36 | clean: true, 37 | out: "dist", 38 | htmlFragments: true 39 | }, 40 | exclude: [ 41 | "**/*.{fs,fsproj}", 42 | "**/bin/**", 43 | "**/obj/**" 44 | ], 45 | /* ... */ 46 | }; -------------------------------------------------------------------------------- /src/website/src/App.fs: -------------------------------------------------------------------------------- 1 | module App 2 | 3 | open Browser.Dom 4 | open Browser.Types 5 | open Fable.Core 6 | open Fable.Core.DynamicExtensions 7 | open Sutil 8 | open Sutil.DOM 9 | open Sutil.Attr 10 | open Sutil.Styling 11 | open Sutil.Shoelace 12 | open Sutil.Fast 13 | open Types 14 | open Router 15 | open Components 16 | open Sutil.Fast.FastDesignSystemProvider 17 | 18 | type State = { page: Page; navOpen: bool } 19 | 20 | type Msg = 21 | | NavigateTo of Page 22 | | ToggleNav 23 | 24 | [] 25 | let registerThemeEventListener (onThemeChange: bool -> unit) = jsNative 26 | 27 | [] 28 | let getPalette (color: string) = jsNative 29 | 30 | let init () : State * Cmd = 31 | { page = Home; navOpen = false }, Cmd.none 32 | 33 | let update (msg: Msg) (state: State) : State * Cmd = 34 | match msg with 35 | | NavigateTo page -> { state with page = page }, Cmd.none 36 | | ToggleNav -> 37 | { state with 38 | navOpen = not state.navOpen }, 39 | Cmd.none 40 | 41 | let navigateTo (dispatch: Dispatch) (page: Page) = 42 | NavigateTo page |> dispatch 43 | 44 | let view () = 45 | let state, dispatch = Store.makeElmish init update ignore () 46 | 47 | let theme = 48 | Store.make 49 | {| luminance = 1. 50 | backgroundColor = "#E1E1E1" |} 51 | 52 | let navigateTo = navigateTo dispatch 53 | 54 | let changeTheme isDark = 55 | if isDark then 56 | document.body.classList.add ("sl-theme-dark") 57 | 58 | theme 59 | <~ {| luminance = 0.23 60 | backgroundColor = "#1E1E1E" |} 61 | else 62 | document.body.classList.remove ("sl-theme-dark") 63 | 64 | theme 65 | <~ {| luminance = 1. 66 | backgroundColor = "#FFFFFF" |} 67 | 68 | registerThemeEventListener changeTheme 69 | 70 | Router.on "/" (fun _ -> navigateTo Home) |> ignore 71 | 72 | Router.on 73 | ":library/docs/:page" 74 | (fun (mtc: Match option) -> 75 | match mtc with 76 | | Some mtc -> 77 | match mtc.data with 78 | | Some { library = library; page = None } -> 79 | let page = "getting-started" 80 | let docs = Docs(library, page) 81 | navigateTo docs 82 | | Some { library = library; page = page } -> 83 | let page = 84 | page |> Option.defaultValue "getting-started" 85 | 86 | let docs = Docs(library, page) 87 | navigateTo docs 88 | | None -> navigateTo Home 89 | | None -> navigateTo Home) 90 | |> ignore 91 | 92 | Router 93 | .notFound(fun _ -> navigateTo NotFound) 94 | .resolve() 95 | 96 | let (|Shoelace|Fast|) = 97 | function 98 | | "fast" -> Fast 99 | | _ -> Shoelace 100 | 101 | let page = 102 | let location = getCurrentLocation () 103 | 104 | match location with 105 | | [||] -> Page.Home 106 | | [| "fast"; "docs"; name |] -> Page.Docs("fast", name) 107 | | [| "shoelace"; "docs"; name |] -> Page.Docs("shoelace", name) 108 | | _ -> Page.NotFound 109 | 110 | navigateTo page 111 | 112 | 113 | let background = 114 | theme .> (fun theme -> theme.backgroundColor) 115 | 116 | let luminance = theme .> (fun theme -> theme.luminance) 117 | 118 | let setColorPallete (e: Event) = 119 | let el = 120 | // We'll cast this heare for clarity although, it is not necessary 121 | e.target :?> FastDesignSystemProvider 122 | // accentPallete is not an attribute hence why we'll assign it dynamically 123 | el.["accentPalette"] <- getPalette "#0ea5e9" 124 | 125 | Html.app [ 126 | disposeOnUnmount [ state ] 127 | Fast.FastDesignSystemProvider [ 128 | class' "main-provider" 129 | onMount setColorPallete [] 130 | Attr.custom ("use-defaults", "true") 131 | Attr.custom ("density", "1") 132 | Attr.custom ("corner-radius", "5") 133 | // workaround we usually would bind to the attribute not the property 134 | Bind.attr ("backgroundColor", background) 135 | Bind.attr ("baseLayerLuminance", luminance) 136 | Html.nav [ 137 | class' "app-nav" 138 | Html.section [ 139 | Shoelace.SlButton [ 140 | type' "text" 141 | text "Sutil.Shoelace" 142 | onClick (fun _ -> Router.navigate "/" None) [] 143 | ] 144 | Shoelace.SlButton [ 145 | type' "text" 146 | text "Shoelace" 147 | Shoelace.SlIcon [ 148 | Attr.name "book" 149 | Attr.slot "prefix" 150 | ] 151 | onClick (fun _ -> Router.navigate "shoelace/docs/index" None) [] 152 | ] 153 | Shoelace.SlButton [ 154 | type' "text" 155 | text "Fast" 156 | Shoelace.SlIcon [ 157 | Attr.name "book" 158 | Attr.slot "prefix" 159 | ] 160 | onClick (fun _ -> Router.navigate "fast/docs/index" None) [] 161 | ] 162 | ] 163 | Html.section [ 164 | class' "show-on-mobile" 165 | Shoelace.SlButton [ 166 | type' "text" 167 | text "Menu" 168 | Shoelace.SlIcon [ 169 | Attr.name "list" 170 | Attr.slot "prefix" 171 | ] 172 | onClick (fun _ -> dispatch ToggleNav) [] 173 | ] 174 | ] 175 | ] 176 | Html.main [ 177 | Desktop.Sidenav 178 | Mobile.Sidenav [ 179 | Bind.attr ("open", (state .> fun state -> state.navOpen)) 180 | on "sl-hide" (fun _ -> dispatch ToggleNav) [] 181 | ] 182 | 183 | bindFragment state 184 | <| fun state -> 185 | match state.page with 186 | | Home -> Pages.Home.view () 187 | | Docs (library, page) -> Pages.Docs.view library page 188 | | Library library -> 189 | match library with 190 | | Shoelace -> Pages.Shoelace.view () 191 | | Fast -> Pages.Fast.view () 192 | | NotFound -> Html.article [ text "NotFound" ] 193 | ] 194 | ] 195 | 196 | ] 197 | -------------------------------------------------------------------------------- /src/website/src/App.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/website/src/Components/Sidenav.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Components.Sidenav 3 | 4 | open Sutil 5 | open Sutil.Attr 6 | open Sutil.Shoelace 7 | 8 | open Routes 9 | open Sutil.DOM 10 | 11 | let shoelace = 12 | routes 13 | |> List.filter (fun route -> route.library = Shoelace) 14 | |> List.groupBy (fun r -> r.category) 15 | 16 | let fast = 17 | routes 18 | |> List.filter (fun route -> route.library = Fast) 19 | |> List.groupBy (fun r -> r.category) 20 | 21 | 22 | module Desktop = 23 | let private libraryMenu (library: list>) = 24 | Html.ul [ 25 | class' "menu-categories" 26 | for (category, routes) in library do 27 | Html.li [ 28 | class' "menu-categories-category" 29 | Html.h4 [ text (category.AsString()) ] 30 | 31 | Html.ul [ 32 | for value in routes do 33 | Html.li [ 34 | Shoelace.SlButton [ 35 | type' "text" 36 | text value.name 37 | Attr.href value.href 38 | ] 39 | ] 40 | ] 41 | ] 42 | ] 43 | 44 | let Sidenav = 45 | Html.aside [ 46 | class' "site-menu desktop" 47 | Html.ul [ 48 | Html.h4 [ 49 | Attr.slot "summary" 50 | text "Shoelace" 51 | ] 52 | libraryMenu shoelace 53 | ] 54 | Html.ul [ 55 | Html.h4 [ 56 | Attr.slot "summary" 57 | text "FAST" 58 | ] 59 | libraryMenu fast 60 | ] 61 | ] 62 | 63 | 64 | 65 | module Mobile = 66 | let private mobileMenu (library: list>) = 67 | Html.ul [ 68 | class' "menu-categories" 69 | for (category, routes) in library do 70 | Html.li [ 71 | class' "menu-categories-category" 72 | Html.h4 [ text (category.AsString()) ] 73 | 74 | Html.ul [ 75 | for value in routes do 76 | Html.li [ 77 | Shoelace.SlButton [ 78 | type' "text" 79 | text value.name 80 | Attr.href value.href 81 | ] 82 | ] 83 | ] 84 | ] 85 | ] 86 | 87 | let Sidenav (content: NodeFactory seq) = 88 | Shoelace.SlDrawer [ 89 | yield! content 90 | class' "site-menu" 91 | Html.label [ 92 | Attr.slot "label" 93 | text "Select a library Documentation" 94 | ] 95 | Html.ul [ 96 | Html.li [ 97 | Html.h3 "Shoelace" 98 | mobileMenu shoelace 99 | ] 100 | ] 101 | Html.ul [ 102 | Html.li [ 103 | Html.h3 "Fast" 104 | mobileMenu fast 105 | ] 106 | ] 107 | ] 108 | -------------------------------------------------------------------------------- /src/website/src/Main.fs: -------------------------------------------------------------------------------- 1 | module Main 2 | 3 | open Sutil 4 | open Sutil.DOM 5 | open Fable.Core 6 | open Fable.Core.JsInterop 7 | open Sutil.Fast.FastDesignSystemProvider 8 | open Sutil.Fast.FastButton 9 | open Sutil.Fast.FastAnchor 10 | 11 | // CSS Imports 12 | 13 | importSideEffects "@shoelace-style/shoelace/dist/themes/base.css" 14 | importSideEffects "@shoelace-style/shoelace/dist/themes/dark.css" 15 | importSideEffects "highlight.js/styles/nord.css" 16 | importSideEffects "firacode/distr/fira_code.css" 17 | importSideEffects "./styles.css" 18 | 19 | // JS Imports 20 | 21 | importSideEffects "@shoelace-style/shoelace/dist/components/alert/alert.js" 22 | importSideEffects "@shoelace-style/shoelace/dist/components/button/button.js" 23 | 24 | importSideEffects 25 | "@shoelace-style/shoelace/dist/components/checkbox/checkbox.js" 26 | 27 | importSideEffects "@shoelace-style/shoelace/dist/components/icon/icon.js" 28 | 29 | importSideEffects 30 | "@shoelace-style/shoelace/dist/components/icon-button/icon-button.js" 31 | 32 | importSideEffects "@shoelace-style/shoelace/dist/components/menu/menu.js" 33 | 34 | importSideEffects 35 | "@shoelace-style/shoelace/dist/components/menu-divider/menu-divider.js" 36 | 37 | importSideEffects 38 | "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js" 39 | 40 | importSideEffects "@shoelace-style/shoelace/dist/components/drawer/drawer.js" 41 | importSideEffects "@shoelace-style/shoelace/dist/components/include/include.js" 42 | 43 | importSideEffects 44 | "@shoelace-style/shoelace/dist/components/dropdown/dropdown.js" 45 | 46 | importSideEffects "./Theme.js" 47 | 48 | let private FASTDesignSystemProvider = 49 | importMember "@microsoft/fast-components" 50 | 51 | let private FASTButton = 52 | importMember "@microsoft/fast-components" 53 | 54 | let private FASTAnchor = 55 | importMember "@microsoft/fast-components" 56 | 57 | 58 | 59 | [] 60 | let setBasePath (path: string) : unit = jsNative 61 | 62 | setBasePath "shoelace" 63 | // Start the app 64 | App.view () |> mountElement "sutil-app" 65 | -------------------------------------------------------------------------------- /src/website/src/Pages/Docs.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Pages.Docs 3 | 4 | open Sutil 5 | open Sutil.DOM 6 | open Sutil.Shoelace 7 | open Sutil.Styling 8 | open Sutil.Attr 9 | 10 | let view (library: string) (page: string) = 11 | Html.article [ 12 | class' "doc-page" 13 | Shoelace.SlInclude [ 14 | Attr.src $"/dist/docs/{library}/{page}.html" 15 | Attr.custom ("allow-scripts", "true") 16 | ] 17 | ] 18 | -------------------------------------------------------------------------------- /src/website/src/Pages/Fast.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Pages.Fast 3 | 4 | open Sutil 5 | 6 | let view () = Pages.Docs.view "fast" "index" 7 | -------------------------------------------------------------------------------- /src/website/src/Pages/Home.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Pages.Home 3 | 4 | open Sutil 5 | open Sutil.Shoelace 6 | open Sutil.Fast 7 | open Sutil.Styling 8 | open Sutil.DOM 9 | open Sutil.Attr 10 | 11 | open type Feliz.length 12 | 13 | let view () = 14 | Html.article [ 15 | class' "home-page" 16 | Html.section [ 17 | Shoelace.SlInclude [ 18 | Attr.src "/dist/docs/home.html" 19 | ] 20 | ] 21 | Html.section [ 22 | class' "row" 23 | Fast.FastAnchor [ 24 | Attr.href "https://fast.design" 25 | Attr.target "_blank" 26 | Attr.custom ("appearance", "outline") 27 | text "Get to know more about FAST" 28 | ] 29 | Shoelace.SlButton [ 30 | Attr.target "_blank" 31 | Attr.href "https://shoelace.style" 32 | text "Get to know more about Shoelace" 33 | ] 34 | ] 35 | Html.section [ 36 | Shoelace.SlInclude [ 37 | Attr.src "/dist/docs/home-2.html" 38 | ] 39 | ] 40 | ] 41 | |> withStyle [ 42 | rule 43 | "section.row" 44 | [ Css.marginTop (em 2) 45 | Css.marginBottom (em 2) 46 | Css.displayFlex 47 | Css.custom ("justify-content", "space-evenly") 48 | Css.alignItemsCenter ] 49 | ] 50 | -------------------------------------------------------------------------------- /src/website/src/Pages/Shoelace.fs: -------------------------------------------------------------------------------- 1 | [] 2 | module Pages.Shoelace 3 | 4 | open Sutil 5 | 6 | let view () = Html.article [ text "" ] 7 | -------------------------------------------------------------------------------- /src/website/src/Router.fs: -------------------------------------------------------------------------------- 1 | module Router 2 | 3 | open Fable.Core 4 | 5 | // handler 6 | // hooks 7 | type Route = {| name: string; path: string |} 8 | 9 | type Match<'UrlParams, 'QueryParams> = 10 | {| url: string 11 | queryString: string 12 | hashString: string 13 | Route: Route 14 | data: 'UrlParams option 15 | ``params``: 'QueryParams option |} 16 | 17 | type RouteHandler<'UrlParams, 'QueryParams> = 18 | Match<'UrlParams, 'QueryParams> option -> unit 19 | 20 | type Router = 21 | abstract member on : 22 | string -> 23 | RouteHandler<'UrlParams, 'QueryParams> -> 24 | Router 25 | 26 | abstract member notFound : (unit -> unit) -> Router 27 | abstract member navigate : string -> obj option -> unit 28 | abstract member navigateByName : string -> 'T option -> unit 29 | abstract member getCurrentLocation : unit -> Match<_, _> 30 | abstract member resolve : unit -> unit 31 | 32 | 33 | [] 34 | let Router : Router = jsNative 35 | 36 | [] 37 | let getCurrentLocation : unit -> string array = jsNative 38 | -------------------------------------------------------------------------------- /src/website/src/Router.js: -------------------------------------------------------------------------------- 1 | import Navigo from 'navigo'; 2 | 3 | export const Router = new Navigo("/", { hash: true }); 4 | 5 | export function getCurrentLocation() { 6 | return Router 7 | .getCurrentLocation() 8 | .hashString 9 | .split("/") 10 | .filter(s => s); 11 | } -------------------------------------------------------------------------------- /src/website/src/Routes.fs: -------------------------------------------------------------------------------- 1 | module Routes 2 | 3 | type Category = 4 | | Guides 5 | | Components 6 | | Uncategorized 7 | 8 | member this.AsString() = 9 | match this with 10 | | Guides -> "Guides" 11 | | Components -> "Components" 12 | | Uncategorized -> "" 13 | 14 | type Library = 15 | | Shoelace 16 | | Fast 17 | 18 | type DocsRoute = 19 | { name: string 20 | href: string 21 | library: Library 22 | category: Category } 23 | 24 | 25 | let routes = 26 | [ { name = "Getting Started" 27 | href = "#/shoelace/docs/getting-started" 28 | library = Shoelace 29 | category = Guides } 30 | { name = "Getting Started" 31 | href = "#/fast/docs/getting-started" 32 | library = Fast 33 | category = Guides } 34 | { name = "Themes" 35 | href = "#/fast/docs/themes" 36 | library = Fast 37 | category = Guides } 38 | { name = "How to use components" 39 | href = "#/shoelace/docs/components" 40 | library = Shoelace 41 | category = Guides } 42 | { name = "Elmish" 43 | href = "#/shoelace/docs/elmish" 44 | library = Shoelace 45 | category = Guides } 46 | { name = "Stores" 47 | href = "#/shoelace/docs/stores" 48 | library = Shoelace 49 | category = Guides } ] 50 | -------------------------------------------------------------------------------- /src/website/src/Theme.js: -------------------------------------------------------------------------------- 1 | import { parseColorString, createColorPalette } from '@microsoft/fast-components'; 2 | const prefersDarkQuery = window.matchMedia('(prefers-color-scheme: dark)'); 3 | const isDark = () => prefersDarkQuery.matches; 4 | export function registerThemeEventListener(cb) { 5 | cb(isDark()); 6 | prefersDarkQuery.addEventListener('change', () => cb(isDark())); 7 | } 8 | 9 | export function getPalette(color) { 10 | return createColorPalette(parseColorString(color)); 11 | } -------------------------------------------------------------------------------- /src/website/src/Types.fs: -------------------------------------------------------------------------------- 1 | module Types 2 | 3 | type Page = 4 | | Home 5 | | Library of string 6 | | Docs of library: string * docSite: string 7 | | NotFound 8 | 9 | type Theme = 10 | | Light 11 | | Dark 12 | 13 | type DocsUrlData = 14 | { library: string 15 | page: string option } 16 | -------------------------------------------------------------------------------- /src/website/src/docs/fast/getting-started.md: -------------------------------------------------------------------------------- 1 | [Javascript Modules]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules 2 | [femto]: https://github.com/Zaid-Ajaj/Femto 3 | [Theming]: #/fast/docs/theming 4 | 5 | # Getting Started 6 | 7 | ```sh 8 | dotnet add package Sutil.Fast --version 9 | pnpm install @microsoft/fast-components@ @microsoft/fast-foundation lodash-es # or npm install @microsoft/Fast@ 10 | ``` 11 | If you are using [femto] then you just need to do 12 | 13 | - `femto install Sutil.Fast --version ` 14 | 15 | # After Install 16 | 17 | > Please feel free to also check Fast's [documentation](https://www.fast.design/docs/introduction/). 18 | 19 | 20 | Once you're done installing Sutil.Fast, you should be able to do the following: 21 | 22 | ```fsharp 23 | open Sutil.Fast 24 | 25 | Fast.FastDesignSystemProvider [ 26 | Attr.custom ("use-defaults", "true") 27 | Attr.style "padding: 1em" 28 | Fast.FastButton [ 29 | Attr.custom("appearance","primary") 30 | text "Hello, there!" 31 | ] 32 | ] 33 | ``` 34 | 35 | but that doesn't mean that you will see yout components right away, Fast uses [Javascript Modules] to load its components. 36 | 37 | To do that we need to load the components in one of the following ways in your `Main.fs` or entry point file of your Sutil application 38 | 39 | ```fsharp 40 | // Main.fs or App.fs 41 | importSideEffects "@microsoft/fast-components" 42 | ``` 43 | This will load all the Fast components to the browser and then you should be able to see the following 44 | 45 | 46 | 47 | Hello, there! 48 | 49 | 50 | Fast uses a design system provider that allows you to easily theme your application or parts of your application as much as you desire, if you want to learn more about that check 51 | 52 | Fast Themes 53 | 54 | For development purposes this should be enough. If you want to optimize for production I'd encourage you to keep reading the next section 55 | ## Cherry-pick components 56 | 57 | > Please feel free to also check Fast's [documentation](https://www.fast.design/docs/components/getting-started#from-npm) 58 | 59 | 60 | To optimize your bundle sizes and prevent unused code ending up in your user's storage, then cherry-picking is the ideal option since you will only load and bundle the components you're using. 61 | 62 | ```fsharp 63 | open Sutil.Fast.FastDesignSystemProvider 64 | open Sutil.Fast.FastButton 65 | open Sutil.Fast.FastAnchor 66 | 67 | let private FASTDesignSystemProvider = importMember"@microsoft/fast-components" 68 | let private FASTButton = importMember"@microsoft/fast-components" 69 | let private FASTAnchor = importMember"@microsoft/fast-components" 70 | ``` 71 | 72 | This will help your bundler to prevent tree-shaking the elements that you're using. 73 | Now you're ready to start doing some Sutil.Fast! -------------------------------------------------------------------------------- /src/website/src/docs/fast/index.md: -------------------------------------------------------------------------------- 1 | [Sutil.Generator]: https://github.com/AngelMunoz/Sutil.Generator 2 | [FAST]: https://fast.design/ 3 | [Sutil]: https://davedawkins.github.io/Sutil/ 4 | [femto]: https://github.com/Zaid-Ajaj/Femto 5 | 6 | # Sutil.Fast 7 | [![NuGet Badge](https://buildstats.info/nuget/Sutil.Fast?includePreReleases=true)](https://www.nuget.org/packages/Sutil.Fast) 8 | 9 | **Sutil.Fast** is a small [Sutil] wrapper built on top of [FAST] a web component library. 10 | 11 | **Sutil.Fast** is autogenerated thanks to the [Sutil.Generator] project. Sutil.Fast also adds some helpers to allow you interact in a seamless way with Sutil 12 | 13 | ### Installation 14 | 15 | ```sh 16 | dotnet add package Sutil.Fast --version 17 | pnpm install @microsoft/fast-components@ @microsoft/fast-foundation lodash-es # or npm install @microsoft/shoelace@ 18 | ``` 19 | If you are using [femto] then you just need to do 20 | 21 | - `femto install Sutil.Fast --version ` 22 | 23 | # Getting Started 24 | 25 | - [Documentation](#/fast/docs/getting-started) 26 | - [Components](#/fast/docs/components) 27 | 28 | 29 | ## Versioning 30 | Sutil.Fast is built against the latest version of [Fast] in case there are bug fixes on the generator itself or additions for quality of life the nuget version will add a last dot and a number to indicate this was a new version of the generated package. 31 | 32 | Example: 33 | - `Fast version: v1.21.6` 34 | 35 | **Normal Release ->** `Sutil.Fast version: v1.21.6` 36 | 37 | - `Fast version: v1.21.6` 38 | **Sutil.Generator Fix/QL Update ->** `Sutil.Fast version: Fast version: v1.21.6.1` 39 | 40 | This is expected to be done in rare ocations when a fix or QL update really is needed, for the most part most of the QL updates will be sent with the next Fast release to prevent distancing from the shoelace version 41 | -------------------------------------------------------------------------------- /src/website/src/docs/fast/themes.md: -------------------------------------------------------------------------------- 1 | [Themes]: https://www.fast.design/docs/design-systems/overview 2 | 3 | # FAST Themes 4 | 5 | > Please also check FAST documentation on [Themes] for more and better information. 6 | 7 | Fast includes a system design provider which allows you to write theming in your website from the start. 8 | 9 | In the case of `Sutil.Fast` once you [have imported](#/fast/docs/getting-started) the required scripts you can do the following 10 | 11 | ```fsharp 12 | open Sutil.Fast 13 | 14 | Fast.FastDesignSystemProvider [ 15 | // initialize all of the properties 16 | Attr.custom ("use-defaults", "true") 17 | Attr.custom ("density", "1") 18 | Attr.custom ("corner-radius", "5") 19 | Attr.custom ("background-color", "#000000") 20 | Attr.custom ("base-layer-luminance", "0.23") 21 | 22 | Fast.FastButton [ 23 | Attr.custom("appearance", "accent") 24 | text "Hey there!" 25 | ] 26 | ] 27 | 28 | ``` 29 | that will give you a dark style with the default accent 30 | 31 | 32 | Hey there! 33 | 34 | 35 | 36 | # Accent color pallete 37 | 38 | > Please also note that FAST uses a lot of javascript for creating custom design systems, so in those cases you are advised to simply add a javascript file for interop (unless you have the time to create bindings for the javascript code in the FAST library) 39 | 40 | 41 | 42 | This part is not obligatory but it might be the easiest way to do it until there's a bindings package for the javascript part of FAST 43 | ```js 44 | import { parseColorString, createColorPalette } from '@microsoft/fast-components'; 45 | 46 | // generate a new color palette for the color you chose 47 | export function getPalette(color) { 48 | return createColorPalette(parseColorString(color)); 49 | } 50 | ``` 51 | 52 | 53 | ```fsharp 54 | open Browser.Types 55 | 56 | open Fable.Core 57 | open Fable.Core.DynamicExtensions 58 | 59 | open Sutil.Fast 60 | open Sutil.Fast.FastDesignSystemProvider 61 | 62 | [] 63 | let getPalette (color: string) = jsNative 64 | 65 | let setColorPallete (e: Event) = 66 | let el = 67 | // We'll cast this heare for clarity although, it is not necessary 68 | e.target :?> FastDesignSystemProvider 69 | // accentPallete is not an attribute hence why we'll assign it dynamically 70 | el.["accentPalette"] <- getPalette "#0ea5e9" 71 | 72 | let view() = 73 | Fast.FastDesignSystemProvider [ 74 | // when the component is mounted set the collor palette 75 | onMount setColorPallete [] 76 | // initialize all of the properties 77 | Attr.custom ("use-defaults", "true") 78 | // set custom values for existing attributes 79 | Attr.custom ("density", "1") 80 | Attr.custom ("corner-radius", "5") 81 | Attr.custom ("background-color", "#000000") 82 | Attr.custom ("base-layer-luminance", "0.23") 83 | 84 | Fast.FastButton [ 85 | Attr.custom("appearance", "accent") 86 | text "Hey there!" 87 | ] 88 | ] 89 | ``` 90 | That should look like this: 91 | 92 | 93 | Hey there! 94 | 95 | 96 | 101 | 102 | # Dark/Light Mode 103 | 104 | Supporting Light and Dark modes is not too hard and should be simple to do. for that we just need to listen for the media query change, add a store and register a callback to react to those changes. 105 | 106 | Let's add a few things to our JS file 107 | 108 | ```js 109 | import { parseColorString, createColorPalette } from '@microsoft/fast-components'; 110 | 111 | const prefersDarkQuery = window.matchMedia('(prefers-color-scheme: dark)'); 112 | const isDark = () => prefersDarkQuery.matches; 113 | 114 | export function registerThemeEventListener(cb) { 115 | cb(isDark()); 116 | prefersDarkQuery.addEventListener('change', () => cb(isDark())); 117 | } 118 | 119 | export function getPalette(color) { 120 | return createColorPalette(parseColorString(color)); 121 | } 122 | ``` 123 | 124 | ```fsharp 125 | open Browser.Types 126 | 127 | open Fable.Core 128 | open Fable.Core.DynamicExtensions 129 | open Sutil 130 | open Sutil.Fast 131 | open Sutil.Fast.FastDesignSystemProvider 132 | 133 | 134 | [] 135 | let registerThemeEventListener (onThemeChange: bool -> unit) = jsNative 136 | 137 | [] 138 | let getPalette (color: string) = jsNative 139 | 140 | let setColorPallete (e: Event) = 141 | let el = 142 | // We'll cast this heare for clarity although, it is not necessary 143 | e.target :?> FastDesignSystemProvider 144 | // accentPallete is not an attribute hence why we'll assign it dynamically 145 | el.["accentPalette"] <- getPalette "#0ea5e9" 146 | 147 | let changeTheme isDark = 148 | // theme <~ is the equivalent to 149 | // Store.set {| ...values ... |} theme 150 | if isDark then 151 | theme 152 | <~ {| luminance = 0.23 153 | backgroundColor = "#1E1E1E" |} 154 | else 155 | theme 156 | <~ {| luminance = 1. 157 | backgroundColor = "#FFFFFF" |} 158 | // register our callback 159 | registerThemeEventListener changeTheme 160 | 161 | let background = 162 | // theme .> (fun theme -> theme.backgroundColor) is the equivalent to 163 | // Store.map (fun theme -> theme.backgroundColor) theme 164 | theme .> (fun theme -> theme.backgroundColor) 165 | 166 | let luminance = theme .> (fun theme -> theme.luminance) 167 | 168 | let view() = 169 | let theme = 170 | Store.make 171 | // Initial values for your theme 172 | {| luminance = 1. 173 | backgroundColor = "#1E1E1E" |} 174 | Fast.FastDesignSystemProvider [ 175 | // when the component is mounted set the collor palette 176 | onMount setColorPallete [] 177 | // initialize all of the properties 178 | Attr.custom ("use-defaults", "true") 179 | // set custom values for existing attributes 180 | Attr.custom ("density", "1") 181 | Attr.custom ("corner-radius", "5") 182 | // workaround we usually would bind to the attribute not the property 183 | // e.g Bind.attr ("background-color", background) 184 | Bind.attr ("backgroundColor", background) 185 | Bind.attr ("baseLayerLuminance", luminance) 186 | 187 | Fast.FastButton [ 188 | Attr.custom("appearance", "accent") 189 | text "Hey there!" 190 | ] 191 | ] 192 | ``` 193 | 194 | That's precisely how this website is configured right now! for the sample we'll inver the colors just to show that you can use multiple providers and have multiple sections with different themes 195 | 196 | 197 | Hey there! 198 | 199 | 200 | 218 | -------------------------------------------------------------------------------- /src/website/src/docs/getting-started.md: -------------------------------------------------------------------------------- 1 | [Javascript Modules]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules 2 | [femto]: https://github.com/Zaid-Ajaj/Femto 3 | 4 | # Getting Started 5 | 6 | ```sh 7 | dotnet add package Sutil.Shoelace --version 8 | pnpm install @shoelace-style/shoelace@ # or npm install @shoelace-style/shoelace@ 9 | ``` 10 | If you are using [femto] then you just need to do 11 | 12 | - `femto install Sutil.Shoelace --version ` 13 | 14 | # After Install 15 | 16 | > Please feel free to also check shoelace's [documentation](https://shoelace.style/getting-started/installation). 17 | 18 | 19 | Once you're done installing Sutil.Shoelace, you should be able to do the following: 20 | 21 | ```fsharp 22 | open Sutil.Shoelace 23 | 24 | Shoelace.SlButton [ 25 | type' "primary" 26 | text "Hello, there!" 27 | ] 28 | ``` 29 | 30 | but that doesn't mean that you will see yout components right away, Shoelace uses [Javascript Modules] to load its components. 31 | 32 | To do that we need to load the components in one of the following ways in your `Main.fs` or entry point file of your Sutil application 33 | 34 | ```fsharp 35 | // Main.fs or App.fs 36 | 37 | importSideEffects "@shoelace-style/shoelace/dist/themes/base.css" 38 | // In case you want to support a dark theme check the guide at 39 | // https://shoelace.style/getting-started/themes?id=dark-mode 40 | // importSideEffects "@shoelace-style/shoelace/dist/themes/dark.css" 41 | 42 | importSideEffects "@shoelace-style/shoelace.js" 43 | ``` 44 | This will load all the shoelace components to the browserand then you should be able to see the following 45 | 46 | Hello, there! 47 | 48 | For development purposes this should be enough. If you want to optimize for production I'd encourage you to keep reading the next section 49 | ## Cherry-pick components 50 | 51 | > Please feel free to also check shoelace's [documentation](https://shoelace.style/getting-started/installation?id=bundling) 52 | 53 | 54 | To optimize your bundle sizes and prevent unused code ending up in your user's storage, then cherry-picking is the ideal option since you will only load and bundle the components you're using. 55 | 56 | ```fsharp 57 | 58 | importSideEffects "@shoelace-style/shoelace/dist/components/button/button.js" 59 | 60 | importSideEffects "@shoelace-style/shoelace/dist/components/icon/icon.js" 61 | ``` 62 | 63 | then if you try the following: 64 | ```fsharp 65 | Shoelace.SlButton [ 66 | Shoelace.SlIcon [ 67 | Attr.slot "prefix" 68 | Attr.name "info-circle" 69 | ] 70 | ] 71 | ``` 72 | it should look like this 73 | 74 | 75 | 76 | 77 | If you wonder why the icon is not showing it means that you're not serving the icons statically 78 | 79 | you can fix that by selecting where are you going to mount the icon assets with your bundler 80 | 81 | ```fsharp 82 | [] 83 | let setBasePath (path: string) : unit = jsNative 84 | 85 | setBasePath "shoelace" 86 | ``` 87 | 88 | 89 | ### Snowpack 90 | In the case of snowpack its quite simple 91 | 92 | ```javascript 93 | /** @type {import("snowpack").SnowpackUserConfig } */ 94 | module.exports = { 95 | 96 | mount: { 97 | public: { url: '/', static: true }, 98 | src: { url: '/dist' }, 99 | // tell snowpack to mount your assets in "shoelace/assets" 100 | 'node_modules/@shoelace-style/shoelace/dist/assets': { url: '/shoelace/assets', static: true } 101 | /* ... the rest of your config */ 102 | }, 103 | /* ... the rest of your config */ 104 | }; 105 | ``` 106 | 107 | ### Webpack 108 | In the case of webpack we can use the copy plugin 109 | ```javascript 110 | module.exports = { 111 | /* ... the rest of your config ... */ 112 | plugins: [ 113 | new CopyPlugin({ 114 | patterns: [ 115 | // Copy Shoelace assets to dist/shoelace 116 | { 117 | from: path.resolve(__dirname, 'node_modules/@shoelace-style/shoelace/dist/assets'), 118 | to: path.resolve(__dirname, 'dist/shoelace/assets') 119 | } 120 | ] 121 | }) 122 | /* ... the rest of your config ... */ 123 | ] 124 | }; 125 | ``` 126 | 127 | Once we've done that either with webpack or snowpack our button will show like this 128 | 129 | 130 | Info 131 | 132 | 133 | Now you're ready to start doing some Sutil.Shoelace! -------------------------------------------------------------------------------- /src/website/src/docs/home-2.md: -------------------------------------------------------------------------------- 1 | If you take a quick inspection in the browser devloper tools you'll find that both libraries work side by side, in fact the whole site has both libraries' elements scattered! and this doesn't really impact on the performance as long as you cherry pick components (check `getting started` on your favorite library). 2 | 3 | If you want to see more details on how these libraries look in F# take a look at the [website docs](https://github.com/AngelMunoz/Sutil.Generator/tree/master/src/website)! -------------------------------------------------------------------------------- /src/website/src/docs/home.md: -------------------------------------------------------------------------------- 1 | [Sutil]: https://github.com/davedawkins/Sutil 2 | [Sutil.Generator]: https://github.com/AngelMunoz/Sutil.Generator 3 | [Web Components]: https://developer.mozilla.org/en-US/docs/Web/Web_Components 4 | [Shoelace]: https://shoelace.style/ 5 | [Fast]: https://fast.design/ 6 | 7 | # Sutil.Generator 8 | 9 | [Sutil.Generator] is a project that aims to bring the goodness of [Web Components] to F# thanks to [Sutil] 10 | 11 | Since Sutil is a pure F# And JS framework without other dependencies rather than Fable.Core and Feliz.Engine it allows itself to be quite interoperable with the rest of the web 12 | 13 | In this page we're using HTML elements, [Shoelace], and [FAST] components, all in the same place they don't fight each other, they just work completely fine 14 | 15 | See them working side by side below 👇🏽 16 | -------------------------------------------------------------------------------- /src/website/src/docs/shoelace/components.md: -------------------------------------------------------------------------------- 1 | [Shoelace]: https://shoelace.style/ 2 | 3 | # Sutil.Shoelace Components 4 | 5 | > For the full list of components please visit [Shoelace]'s website even though the components have been annotated with doc coments so VSCode, Rider or VS2019 pick them up you can follow the original docs to guide you through the components 6 | 7 | We build `Sutil.Shoelace` against a metadata file provided by the npm package, and we follow a few conventions to keep the API consistent and F# friendly 8 | 9 | ```html 10 | 11 | I Agree to send some feedback on this lackluster docs website 12 | 13 | 14 | This is a standard alert. You can customize its content and even the icon. 15 | 16 | ``` 17 | In the case of Sutil.Shoelace we adopt the naming convention used for their React bindings 18 | 19 | ```fsharp 20 | // prefix with `Sl` 21 | Shoelace.SlAlert 22 | Shoelace.SlButton 23 | Shoelace.SlCheckbox 24 | ``` 25 | 26 | We'll show a more complete example below. 27 | 28 | ```fsharp 29 | open Sutil.Shoelace 30 | // Optional open the static class so you don't need to type 31 | // Shoelace.Sl all the time 32 | open type Shoelace.Shoelace 33 | // If you need to access the element's attributes or the 34 | // binding to the native Web Component open the module 35 | // open Sutil.Shoelace. 36 | open Sutil.Shoelace.Alert 37 | open Sutil.Shoelace.Checkbox 38 | 39 | // the simple call signature is like any other Feliz' flavored DSL 40 | // pass a sequence of attributes/nodes to the component 41 | Shoelace.SlButton [ 42 | // with open type Shoelace.Shoelace 43 | SlIcon [ 44 | // many components have slots check shoelace's documentation website! 45 | Attr.slot "prefix" 46 | name "info-circle" 47 | ] 48 | text "My Button" 49 | ] 50 | 51 | let onChkChange (e: Browser.Types.Event) = 52 | // the SlCheckbox is a binding of the native Web Component 53 | // and inside Sutil.Shoelace.Checkbox 54 | let target = (e.target :?> SlCheckbox) 55 | printfn $"{target.``checked``}" 56 | 57 | SlCheckbox [ 58 | text "I Agree to send some feedback on this lackluster docs website" 59 | // listen to events on the elements and their children 60 | // (no need to drill callback props!) 61 | on "sl-change" onChkChange [] 62 | ] 63 | 64 | // once you open a particular component module you'll have access to it's attributes 65 | let alertAttrs = 66 | Store.make ( 67 | // create an attribute record and set the initial values 68 | SlAlertAttributes.create () 69 | |> SlAlertAttributes.withOpen false 70 | |> SlAlertAttributes.withType "primary" 71 | |> SlAlertAttributes.withClosable false 72 | ) 73 | 74 | // the call signature is different, pass an IStore 75 | // then your nodes, becareful you might override an existing binding from the 76 | // attributes here if you try to put it again 77 | SlAlert( 78 | alertAttrs, 79 | [ SlIcon [ 80 | Attr.slot "icon" 81 | Attr.name "info-circle" 82 | ] 83 | text 84 | "This is a standard alert. You can customize its content and even the icon." ] 85 | ) 86 | ``` 87 | That's the general idea of how to use them, there is not much more to it. 88 | 89 | Feel free to send some feedback in case something is weird or not comprehensible -------------------------------------------------------------------------------- /src/website/src/docs/shoelace/elmish.md: -------------------------------------------------------------------------------- 1 | [Stores]: #/shoelace/docs/stores 2 | 3 | # Elmish 4 | 5 | > To check how to work without elmish check out [Stores] 6 | 7 | Working with elmish is fairly simple Sutil provides a few Store helpers that allow you to use elmish based components in a sutil'ish way 8 | 9 | ```fsharp 10 | let state, dispatch = Store.makeElmishSimple init update ignore () 11 | // and 12 | let state, dispatch = Store.makeElmish init update ignore () 13 | ``` 14 | Differences: 15 | - `state` is now an `IStore` rather than just `State` 16 | - we can also dispose things when this elmish component is disposed (pass a function which disposes things instead of ignore) 17 | - The initial params are the last parameter 18 | 19 | Let's say for example you have the following elmish component: 20 | 21 | ```fsharp 22 | open Sutil 23 | open Sutil.Store 24 | open Sutil.Attr 25 | open Sutil.DOM 26 | // you can open Shoelace statically as well! 27 | open Sutil.Shoelace 28 | 29 | type State = { isOpen: bool } 30 | 31 | type Msg = 32 | | SetIsOpen of bool 33 | 34 | let init() = 35 | { isOpen = false } 36 | 37 | let update (msg: Msg) (state: State) = 38 | match msg with 39 | | SetIsOpen isOpen -> { state with isOpen = isOpen } 40 | 41 | let view() = 42 | let state, dispatch = Store.makeElmishSimple init update ignore () 43 | Html.article [ 44 | // don't forget to disposte the store 45 | // when the component gets disposed 46 | disposeOnUnmount [ state ] 47 | 48 | Shoelace.SlButton [ text "Open Externally" ] 49 | Shoelace.SlMenu [ 50 | Shoelace.SlButton [ Attr.slot "trigger"; Attr.custom("caret", "true"); text "Edit" ] 51 | Shoelace.SlMenu [ 52 | Shoelace.SlMenuItem [ text "Cut" ] 53 | Shoelace.SlMenuItem [ text "Copy" ] 54 | Shoelace.SlMenuItem [ text "Paste" ] 55 | Shoelace.SlMenuDivider [] 56 | Shoelace.SlMenuItem [ text "Find" ] 57 | Shoelace.SlMenuItem [ text "Replace" ] 58 | ] 59 | ] 60 | ] 61 | ``` 62 | If you click on the menu, it will work by itself, but if you wanted to do that from a different element that is not the menu, you'd like to trace such event with the elmish loop 63 | 64 |
65 | Open Externally 66 | 67 | Edit 68 | 69 | Cut 70 | Copy 71 | Paste 72 | 73 | Find 74 | Replace 75 | 76 | 77 |
78 | 79 | the normal thing here would be to dispatch an event 80 | 81 | ```fsharp 82 | Shoelace.SlButton [ text "Open Externally"; onClick (fun _ -> dispatch (SetIsOpen true)) [] ] 83 | ``` 84 | this would trigger our elmish update, let's check how to bind that to our components 85 | 86 | ```fsharp 87 | let state, dispatch = Store.makeElmishSimple init update ignore () 88 | // We'll create an observable that reads only the isOpen property 89 | let isOpenHelper = Store.map (fun state -> state.isOpen) state 90 | 91 | Html.article [ 92 | Shoelace.SlButton [ text "Open Externally"; onClick (fun _ -> dispatch (SetIsOpen true)) [] ] 93 | Shoelace.SlMenu [ 94 | // Bind.attr takes an observable 95 | // each time isOpen changes it will also flect these changes on the element 96 | Bind.attr("open", isOpenHelper) 97 | Shoelace.SlButton [ Attr.slot "trigger"; Attr.custom("caret", "true"); text "Edit" ] 98 | // ... omit the rest for brebity ... 99 | ] 100 | ] 101 | ``` 102 | 103 |
104 | Open Externally 105 | 106 | Edit 107 | 108 | Cut 109 | Copy 110 | Paste 111 | 112 | Find 113 | Replace 114 | 115 | 116 |
117 | 118 | You can use this technique to bind any kind of value you want, be it either checked, open, closable, label you name it, binding stores and observables fits pretty well the elmish way of doing things 119 | 120 | ## Complete sample 121 | 122 | ```fsharp 123 | open Sutil 124 | open Sutil.Store 125 | open Sutil.Attr 126 | open Sutil.DOM 127 | // this time we'll do an open type 128 | open type Sutil.Shoelace 129 | 130 | type State = { isOpen: bool } 131 | 132 | type Msg = 133 | | SetIsOpen of bool 134 | 135 | let init() = 136 | { isOpen = false } 137 | 138 | let update (msg: Msg) (state: State) = 139 | match msg with 140 | | SetIsOpen isOpen -> { state with isOpen = isOpen } 141 | 142 | let view() = 143 | let state, dispatch = Store.makeElmishSimple init update ignore () 144 | // We'll create an observable that reads only the isOpen property 145 | let isOpen = Store.map (fun state -> state.isOpen) state 146 | 147 | Html.article [ 148 | // don't forget to disposte the store 149 | // when the component gets disposed 150 | disposeOnUnmount [ state ] 151 | 152 | SlButton [ 153 | text "Open Externally" 154 | // trigger the elmish update 155 | onClick (fun _ -> dispatch (SetIsOpen true)) [] 156 | ] 157 | SlMenu [ 158 | // bind the observable 159 | Bind.attr("open", isOpen) 160 | SlButton [ Attr.slot "trigger"; Attr.custom("caret", "true"); text "Edit" ] 161 | SlMenu [ 162 | SlMenuItem [ text "Cut" ] 163 | SlMenuItem [ text "Copy" ] 164 | SlMenuItem [ text "Paste" ] 165 | SlMenuDivider [] 166 | SlMenuItem [ text "Find" ] 167 | SlMenuItem [ text "Replace" ] 168 | ] 169 | ] 170 | ] 171 | ``` -------------------------------------------------------------------------------- /src/website/src/docs/shoelace/getting-started.md: -------------------------------------------------------------------------------- 1 | [Javascript Modules]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules 2 | [femto]: https://github.com/Zaid-Ajaj/Femto 3 | 4 | # Getting Started 5 | 6 | ```sh 7 | dotnet add package Sutil.Shoelace --version 8 | pnpm install @shoelace-style/shoelace@ # or npm install @shoelace-style/shoelace@ 9 | ``` 10 | If you are using [femto] then you just need to do 11 | 12 | - `femto install Sutil.Shoelace --version ` 13 | 14 | # After Install 15 | 16 | > Please feel free to also check shoelace's [documentation](https://shoelace.style/getting-started/installation). 17 | 18 | 19 | Once you're done installing Sutil.Shoelace, you should be able to do the following: 20 | 21 | ```fsharp 22 | open Sutil.Shoelace 23 | 24 | Shoelace.SlButton [ 25 | type' "primary" 26 | text "Hello, there!" 27 | ] 28 | ``` 29 | 30 | but that doesn't mean that you will see yout components right away, Shoelace uses [Javascript Modules] to load its components. 31 | 32 | To do that we need to load the components in one of the following ways in your `Main.fs` or entry point file of your Sutil application 33 | 34 | ```fsharp 35 | // Main.fs or App.fs 36 | 37 | importSideEffects "@shoelace-style/shoelace/dist/themes/base.css" 38 | // In case you want to support a dark theme check the guide at 39 | // https://shoelace.style/getting-started/themes?id=dark-mode 40 | // importSideEffects "@shoelace-style/shoelace/dist/themes/dark.css" 41 | 42 | importSideEffects "@shoelace-style/shoelace.js" 43 | ``` 44 | This will load all the shoelace components to the browserand then you should be able to see the following 45 | 46 | Hello, there! 47 | 48 | For development purposes this should be enough. If you want to optimize for production I'd encourage you to keep reading the next section 49 | ## Cherry-pick components 50 | 51 | > Please feel free to also check shoelace's [documentation](https://shoelace.style/getting-started/installation?id=bundling) 52 | 53 | 54 | To optimize your bundle sizes and prevent unused code ending up in your user's storage, then cherry-picking is the ideal option since you will only load and bundle the components you're using. 55 | 56 | ```fsharp 57 | 58 | importSideEffects "@shoelace-style/shoelace/dist/components/button/button.js" 59 | 60 | importSideEffects "@shoelace-style/shoelace/dist/components/icon/icon.js" 61 | ``` 62 | 63 | then if you try the following: 64 | ```fsharp 65 | Shoelace.SlButton [ 66 | Shoelace.SlIcon [ 67 | Attr.slot "prefix" 68 | Attr.name "info-circle" 69 | ] 70 | ] 71 | ``` 72 | it should look like this 73 | 74 | 75 | 76 | 77 | If you wonder why the icon is not showing it means that you're not serving the icons statically 78 | 79 | you can fix that by selecting where are you going to mount the icon assets with your bundler 80 | 81 | ```fsharp 82 | [] 83 | let setBasePath (path: string) : unit = jsNative 84 | 85 | setBasePath "shoelace" 86 | ``` 87 | 88 | 89 | ### Snowpack 90 | In the case of snowpack its quite simple 91 | 92 | ```javascript 93 | /** @type {import("snowpack").SnowpackUserConfig } */ 94 | module.exports = { 95 | 96 | mount: { 97 | public: { url: '/', static: true }, 98 | src: { url: '/dist' }, 99 | // tell snowpack to mount your assets in "shoelace/assets" 100 | 'node_modules/@shoelace-style/shoelace/dist/assets': { url: '/shoelace/assets', static: true } 101 | /* ... the rest of your config */ 102 | }, 103 | /* ... the rest of your config */ 104 | }; 105 | ``` 106 | 107 | ### Webpack 108 | In the case of webpack we can use the copy plugin 109 | ```javascript 110 | module.exports = { 111 | /* ... the rest of your config ... */ 112 | plugins: [ 113 | new CopyPlugin({ 114 | patterns: [ 115 | // Copy Shoelace assets to dist/shoelace 116 | { 117 | from: path.resolve(__dirname, 'node_modules/@shoelace-style/shoelace/dist/assets'), 118 | to: path.resolve(__dirname, 'dist/shoelace/assets') 119 | } 120 | ] 121 | }) 122 | /* ... the rest of your config ... */ 123 | ] 124 | }; 125 | ``` 126 | 127 | Once we've done that either with webpack or snowpack our button will show like this 128 | 129 | 130 | Info 131 | 132 | 133 | Now you're ready to start doing some Sutil.Shoelace! -------------------------------------------------------------------------------- /src/website/src/docs/shoelace/index.md: -------------------------------------------------------------------------------- 1 | [Sutil.Generator]: https://github.com/AngelMunoz/Sutil.Generator 2 | [Shoelace]: https://shoelace.style/ 3 | [Sutil]: https://davedawkins.github.io/Sutil/ 4 | [official React components]: https://shoelace.style/getting-started/usage?id=react 5 | [femto]: https://github.com/Zaid-Ajaj/Femto 6 | 7 | # Sutil.Shoelace 8 | [![NuGet Badge](https://buildstats.info/nuget/Sutil.Shoelace?includePreReleases=true)](https://www.nuget.org/packages/Sutil.Shoelace) 9 | 10 | **Sutil.Shoelace** is a small [Sutil] wrapper built on top of [Shoelace] a web component library. 11 | 12 | **Sutil.Shoelace** is autogenerated thanks to the [Sutil.Generator] project which takes a similar approach used to generate the [official React components]. Sutil.Shoelace also adds some helpers to allow you interact in a seamless way with Sutil 13 | 14 | ### Installation 15 | 16 | ```sh 17 | dotnet add package Sutil.Shoelace --version 18 | pnpm install @shoelace-style/shoelace@ # or npm install @shoelace-style/shoelace@ 19 | ``` 20 | If you are using [femto] then you just need to do 21 | 22 | - `femto install Sutil.Shoelace --version ` 23 | 24 | # Getting Started 25 | 26 | - [Documentation](#/docs/getting-started) 27 | - [Components](#/docs/components) 28 | 29 | 30 | ## Versioning 31 | Sutil.Shoelace is built against the latest version of [Shoelace] in case there are bug fixes on the generator itself or additions for quality of life the nuget version will add a last dot and a number to indicate this was a new version of the generated package. 32 | 33 | Example: 34 | - `Shoelace version: v2.0.0-beta.43` 35 | 36 | **Normal Release ->** `Sutil.Shoelace version: v2.0.0-beta.43` 37 | 38 | - `Shoelace version: v2.0.0-beta.43` 39 | **Sutil.Generator Fix/QL Update ->** `Sutil.Shoelace version: Shoelace version: v2.0.0-beta.43.1` 40 | 41 | This is expected to be done in rare ocations when a fix or QL update really is needed, for the most part most of the QL updates will be sent with the next Shoelace release to prevent distancing from the shoelace version 42 | -------------------------------------------------------------------------------- /src/website/src/docs/shoelace/stores.md: -------------------------------------------------------------------------------- 1 | [Elmish]: #/shoelace/docs/elmish 2 | # Stores 3 | 4 | > To check how to work with elmish check out [Elmish] 5 | 6 | Sutil uses the concept of stores (observable objects that *store* state) to manage state in a web application (or part of it) and they are really convenient when it comes to reflect state updates in the UI, and since they don't require a lot of boilerplate it might be the preferred way for smaller components to handle state. 7 | 8 | Let's see with an example. We'll try to open and close an ***Alert*** which looks like this: 9 | ```fsharp 10 | open Sutil 11 | open Sutil.Store 12 | open Sutil.Attr 13 | open Sutil.DOM 14 | // you can open type Shoelace well! 15 | open type Sutil.Shoelace 16 | 17 | let view() = 18 | Html.article [ 19 | SlAlert [ 20 | Attr.custom("open", "true") 21 | Attr.custom("closable", "true") 22 | SlIcon [ Attr.slot "icon"; Attr.name "info-circle" ] 23 | text "This is a standard alert. You can customize its content and even the icon." 24 | ] 25 | ] 26 | ``` 27 | This is a standard alert. You can customize its content and even the icon. 28 | 29 | If you click on the close button it effectively closes the alert, but how can we open that again programatically? 30 | The answer is by binding the `open` attribute (and this technique applies to any other component/element and any other attribute/property). 31 | 32 | We'll now add a checkbox that will toggle the alert open and closed also, it will check/uncheck itself everytime the alert opens or closes. 33 | To accomplish that we'll use a single store for both elements 34 | 35 | ```fsharp 36 | let view() = 37 | // create an store with a bool value 38 | let isOpen = Store.make true 39 | Html.article [ 40 | // don't forget to disposte the store 41 | // when the component gets disposed 42 | disposeOnUnmount [ isOpen ] 43 | SlCheckbox [ 44 | /// Notice the`Bind.attr` 45 | Bind.attr("checked", isOpen) 46 | text "Toggle Alert" 47 | ] 48 | SlAlert [ 49 | /// Notice the`Bind.attr` 50 | Bind.attr("open", isOpen) 51 | Attr.custom("closable", "true") 52 | SlIcon [ Attr.slot "icon"; Attr.name "info-circle" ] 53 | text "This is a standard alert. You can customize its content and even the icon." 54 | ] 55 | ] 56 | ``` 57 | Toggle Alert 58 | 59 | This is a standard alert. You can customize its content and even the icon. 60 | 61 | 62 | In this case that's all we need to do and the reason is that both `SlAlert` and `SlCheckbox` [reflect](https://lit.dev/docs/components/properties/#reflected-attributes) their `open` and `checked` attribute accordingly and the Sutil binding engine tracks those changes and updates the store. 63 | 64 | 65 | ```fsharp 66 | open Sutil 67 | open Sutil.Store 68 | open Sutil.Attr 69 | open Sutil.DOM 70 | // you can open type Shoelace well! 71 | open type Sutil.Shoelace 72 | 73 | let view() = 74 | // create an store with a bool value 75 | let isOpen = Store.make true 76 | Html.article [ 77 | // don't forget to disposte the store 78 | // when the component gets disposed 79 | disposeOnUnmount [ isOpen ] 80 | SlCheckbox [ 81 | Bind.attr("checked", isOpen) 82 | text "Toggle Alert" 83 | ] 84 | SlAlert [ 85 | Bind.attr("open", isOpen) 86 | Attr.custom("closable", "true") 87 | SlIcon [ Attr.slot "icon"; Attr.name "info-circle" ] 88 | text "This is a standard alert. You can customize its content and even the icon." 89 | ] 90 | ] 91 | ``` 92 | 93 | 94 | #### Quality of life improvement helpers 95 | 96 | Sutil.Shoelace adds a standard way to bind multiple attributes at once in a single store because it might get cumbersome when you have multiple stores tracking multiple values all over the place. 97 | 98 | 99 | ```fsharp 100 | open Sutil 101 | open Sutil.DOM 102 | open Sutil.Attr 103 | open Sutil.Shoelace 104 | open type Shoelace.Shoelace 105 | // Access SlAlertAttributes 106 | open Sutil.Shoelace.Alert 107 | // Access SlCheckbox 108 | open Sutil.Shoelace.Checkbox 109 | 110 | let view (page: string) = 111 | let alertAttrs = 112 | Store.make ( 113 | // create an attribute record and set the initial values 114 | SlAlertAttributes.create () 115 | |> SlAlertAttributes.withOpen false 116 | |> SlAlertAttributes.withType "primary" 117 | |> SlAlertAttributes.withClosable false 118 | ) 119 | 120 | let onChkChange (e: Browser.Types.Event) = 121 | // the SlCheckbox is a binding of the native Web Component 122 | let target = (e.target :?> SlCheckbox) 123 | Store.modify (SlAlertAttributes.withClosable target.``checked``) alertAttrs 124 | 125 | let onBtnClick _ = 126 | // try to open the alert if it has been closed 127 | Store.modify (SlAlertAttributes.withOpen true) alertAttrs 128 | 129 | Html.article [ 130 | disposeOnUnmount [ alertAttrs ] 131 | SlCheckbox [ 132 | text "Closable" 133 | // react to multiple events that might be affecting your components 134 | on "sl-change" onChkChange [] 135 | ] 136 | SlButton [ 137 | text "Open Alert" 138 | // react to multiple events that might be affecting your components 139 | onClick onBtnClick [] 140 | ] 141 | SlAlert( 142 | // the call signature is different, pass an IStore 143 | alertAttrs, 144 | // then your nodes, which can include attributes and other components/html elements 145 | [ SlIcon [ 146 | Attr.slot "icon" 147 | Attr.name "info-circle" 148 | ] 149 | text 150 | "This is a standard alert. You can customize its content and even the icon." ] 151 | ) 152 | ] 153 | ``` 154 | > If you try to bind an existing attribute when using the `IStore, NodeFactory seq` signature (e.g. `Bind.attr("closable", myotherStore)`) you might run into weird behavior given that the `SlAttributes` types bind all of the the existing documented attributes here's a sample of the generated code 155 | > 156 | > ```fsharp 157 | > let stateful (attrs: IStore) (nodes: NodeFactory seq) = 158 | > /// here `.>` acts as an operator for `Store.map (fun attrs -> attr.prop) attrs` 159 | > let closable = attrs .> (fun attrs -> attrs.closable) 160 | > let duration = attrs .> (fun attrs -> attrs.duration) 161 | > let open' = attrs .> (fun attrs -> attrs.open') 162 | > let type' = attrs .> (fun attrs -> attrs.type') 163 | > stateless 164 | > [ Bind.attr("closable", closable) 165 | > Bind.attr("duration", duration) 166 | > Bind.attr("open", open') 167 | > Bind.attr("type", type') 168 | > // We merge your nodes at the end 169 | > // so you can take further control of the component 170 | > yield! nodes ] 171 | > ``` 172 | 173 | This is an example that might show when you want to do something like this also, keep in mind that since you're using `Stores` (`Observables`) you should be able to filter, map, different values to different stores with some of the Store's methods. The sky is the limit 174 | 175 | 176 | -------------------------------------------------------------------------------- /src/website/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can use usual css as well */ 2 | 3 | :root { 4 | --su-color: var(--sl-color-primary-500); 5 | --su-background-color: var(--sl-color-white); 6 | } 7 | 8 | 9 | html, body { 10 | margin: 0; 11 | padding: 0; 12 | width: 100vw; 13 | height: 100vh; 14 | color: var(--su-color); 15 | background-color: var(--su-background-color); 16 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 17 | } 18 | 19 | main { 20 | display: flex; 21 | justify-content: center; 22 | } 23 | fast-design-system-provider.main-provider { 24 | height: 100vh; 25 | width: 100vw; 26 | overflow-y: auto; 27 | } 28 | 29 | 30 | a { 31 | color: var(--sl-color-primary-600); 32 | } 33 | a:visited { 34 | color: var(--sl-color-info-500); 35 | } 36 | .app-nav { 37 | display: flex; 38 | flex-wrap: wrap; 39 | border-bottom: 2px solid; 40 | padding-bottom: 1em; 41 | justify-content: space-between; 42 | } 43 | 44 | pre, blockquote { 45 | background-color: var(--sl-color-success-50); 46 | padding: 0.5em; 47 | } 48 | 49 | pre { 50 | overflow-x: auto; 51 | } 52 | 53 | blockquote { 54 | border-left: 5px solid var(--su-color); 55 | } 56 | 57 | .site-menu { 58 | flex-direction: column; 59 | margin: 0 1em; 60 | } 61 | 62 | .site-menu.desktop { 63 | display: none; 64 | } 65 | 66 | .show-on-mobile { 67 | display: none; 68 | } 69 | 70 | .menu-categories { 71 | padding-left: 0; 72 | } 73 | 74 | .menu-categories .menu-categories-category { 75 | list-style-type: none; 76 | } 77 | 78 | article.doc-page, article.home-page { 79 | width: 82vw; 80 | display: flex; 81 | margin-right: auto; 82 | margin-left: auto; 83 | margin-bottom: 2em; 84 | } 85 | .home-page { 86 | flex-direction: column; 87 | } 88 | 89 | article.doc-page sl-include { 90 | overflow-y: auto; 91 | flex: 1 0; 92 | } 93 | 94 | @media (prefers-color-scheme: dark) { 95 | :root { 96 | --su-background-color: #1e1e1e; 97 | --su-color: var(--sl-color-gray-100); 98 | --sl-panel-background-color: var(--su-background-color) 99 | } 100 | pre { 101 | background-color: var(--sl-color-gray-950); 102 | } 103 | blockquote { 104 | color: var(--sl-color-gray-50); 105 | background-color: var(--sl-color-gray-800); 106 | } 107 | sl-menu-item::part(label) { 108 | color: var(--su-color); 109 | } 110 | 111 | sl-alert::part(base) { 112 | color: var(--su-color); 113 | background-color: var(--sl-panel-background-color); 114 | } 115 | } 116 | 117 | @media screen and (min-width: 1024px) { 118 | .site-menu.desktop { 119 | display: flex; 120 | margin-right: auto; 121 | flex: 0 1; 122 | } 123 | } 124 | 125 | @media screen and (min-width: 1024px) { 126 | .site-menu.desktop { 127 | display: flex; 128 | margin-right: auto; 129 | flex: 0 1; 130 | } 131 | } 132 | @media screen and (min-width: 1220px) { 133 | article.home-page, 134 | article.doc-page, 135 | article.home-page section.row { 136 | width: 62vw; 137 | margin-left: unset; 138 | } 139 | } 140 | 141 | @media screen and (min-width: 1024px) { 142 | article.home-page, 143 | article.doc-page, 144 | article.home-page section.row { 145 | width: 66vw; 146 | margin-left: unset; 147 | } 148 | } 149 | 150 | @media screen and (max-width: 502px) { 151 | .show-on-mobile { 152 | display: initial; 153 | } 154 | article.home-page section.row { 155 | flex-direction: column; 156 | align-items: stretch; 157 | } 158 | } 159 | 160 | pre, code { 161 | font-family: 'Fira Code', monospace; 162 | } --------------------------------------------------------------------------------