From fd97d2167e679833eb4922222c988743ac283b48 Mon Sep 17 00:00:00 2001 From: Steve Dunham Date: Fri, 3 Apr 2026 20:36:40 -0700 Subject: [PATCH] todomvc --- .gitignore | 1 + src/Lib/Elab.newt | 2 +- src/Prelude.newt | 2 - src/Web/Spruce.newt | 113 +++++++++ todomvc/.gitignore | 2 + todomvc/README.md | 23 ++ todomvc/css-license.txt | 397 ++++++++++++++++++++++++++++++ todomvc/public/index.html | 13 + todomvc/public/style.css | 393 +++++++++++++++++++++++++++++ {web => todomvc}/src/Prelude.newt | 0 todomvc/src/Todo.newt | 190 ++++++++++++++ todomvc/src/Web | 1 + web/.gitignore | 1 - web/README.md | 12 - web/dist/empty.js | 1 - web/dist/index.html | 11 - web/dist/spruce.ts | 126 ---------- web/dist/style.css | 13 - web/src/Spruce.newt | 31 --- web/src/Todo.newt | 84 ------- 20 files changed, 1134 insertions(+), 282 deletions(-) create mode 100644 src/Web/Spruce.newt create mode 100644 todomvc/.gitignore create mode 100644 todomvc/README.md create mode 100644 todomvc/css-license.txt create mode 100644 todomvc/public/index.html create mode 100644 todomvc/public/style.css rename {web => todomvc}/src/Prelude.newt (100%) create mode 100644 todomvc/src/Todo.newt create mode 120000 todomvc/src/Web delete mode 100644 web/.gitignore delete mode 100644 web/README.md delete mode 100644 web/dist/empty.js delete mode 100644 web/dist/index.html delete mode 100644 web/dist/spruce.ts delete mode 100644 web/dist/style.css delete mode 100644 web/src/Spruce.newt delete mode 100644 web/src/Todo.newt diff --git a/.gitignore b/.gitignore index a8ca8a4..e9134e3 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ src/Revision.newt bootstrap/serializer.js /newt-vscode-lsp/src/newt.js /playground/src/newt.js +.vite diff --git a/src/Lib/Elab.newt b/src/Lib/Elab.newt index 147b1d9..7aa5260 100644 --- a/src/Lib/Elab.newt +++ b/src/Lib/Elab.newt @@ -1426,7 +1426,7 @@ updateRec ctx fc clauses arg ty = do getTele Nothing (VPi _ _ _ _ a b) = do a <- forceType ctx.env a getTele (Just $ RVar fc "$ru") a - getTele Nothing v = error fc "Expected a pi type, got \{show v}" + getTele Nothing v = error fc "Expected \{show v}, missing argument to record update." getTele (Just tm) v = error (getFC tm) "Expected a record type, got \{show v}" infer : Context -> Raw -> M (Tm × Val) diff --git a/src/Prelude.newt b/src/Prelude.newt index ddca667..c419793 100644 --- a/src/Prelude.newt +++ b/src/Prelude.newt @@ -282,8 +282,6 @@ instance Eq String where instance Eq Char where a == b = eqChar a b - - ptype Array : U → U pfunc listToArray : ∀ a. List a → Array a := ` (a, l) => { diff --git a/src/Web/Spruce.newt b/src/Web/Spruce.newt new file mode 100644 index 0000000..fddfcca --- /dev/null +++ b/src/Web/Spruce.newt @@ -0,0 +1,113 @@ +module Web.Spruce + +-- Spruce is not Elm + +-- TODO better story for import.. +-- also, we'll probably want to expose something with subscriptions / dispatch / ... + +import Prelude + +ptype Element +pfunc getElementById : String → Element := `(id) => document.getElementById(id)` + +-- Make this align with spruce.ts +data Attr msg = SAttr String String | MAttr String msg | VAttr String (String → msg) +data VNode msg = TNode String | ENode String (List $ Attr msg) (List $ VNode msg) + +text : ∀ msg. String → VNode msg +text = TNode + +tag : ∀ msg. String → List (Attr msg)→ List (VNode msg) → VNode msg +tag = ENode + +tag_ : ∀ msg. String → List (VNode msg)→ VNode msg +tag_ tag es = ENode tag Nil es + +pfunc runApp : ∀ msg model. Element → model → (update : msg → model → model) → (view : model → VNode msg) → Unit := ` +(_msg, _model, node, init, update, view) => { + function replace(parent, node, child) { + if (node) { + parent.insertBefore(child, node); + parent.removeChild(node); + } + else { + parent.appendChild(child); + } + return child; + } + // patch node, possibly returning a new node + function patch(parentNode, node, vnode) { + if (vnode.tag == 0) { + if (node && node.nodeType == 3) { + node.nodeValue = vnode.h1; + return node; + } + return replace(parentNode, node, document.createTextNode(vnode.h1)); + } + let el; + if (node instanceof Element && node.tagName.toLowerCase() == vnode.h1) { + el = node; + } + else { + el = document.createElement(vnode.h1); + } + // update node here + let has = {}; + for (let attrs = vnode.h2; attrs.tag == 1; attrs = attrs.h2) { + let attr = attrs.h1; + if (attr.tag == 0) { + let key = attr.h1; + has[key] = true; + if (key in el) + el[key] = attr.h2; + else + el.setAttribute(key, attr.h2); + } else { + let key = attr.h1.slice(2).toLowerCase(); + has[key] = true; + let events = el.events || (el.events = {}); + if (!events[key]) + el.addEventListener(key, listener); + events[key] = attr; + } + } + for (let i = 0; i < el.attributes.length;) { + let attr = el.attributes[i]; + if (!has[attr.name]) { + el.removeAttribute(attr.name); + } else { + i++; + } + } + if (el.events) { + for (let key of Object.keys(el.events)) { + if (!has[key]) delete el.events[key]; + } + } + let i = 0; + for (let kids = vnode.h3; kids.tag == 1; kids = kids.h2) { + patch(el, el.childNodes[i++], kids.h1); + } + while (el.childNodes[i]) el.removeChild(el.childNodes[i]); + return node == el ? node : replace(parentNode, node, el); + } + let model = init; + let vdom = view(model); + let listener = (ev) => { + console.log('listener', ev, ev.target); + let target = ev.target; + for (;;) { + if (!target) return; + if (target.events?.[ev.type]) break; + target = target.parentElement; + } + const attr = target.events[ev.type]; + let action = attr.tag == 2 ? attr.h2(ev.target.value) : attr.h2; + model = update(action)(model); + vdom = view(model); + node = patch(node.parentNode, node, vdom); + }; + node = patch(node.parentNode, node, vdom); + return 0 +} +` diff --git a/todomvc/.gitignore b/todomvc/.gitignore new file mode 100644 index 0000000..4813571 --- /dev/null +++ b/todomvc/.gitignore @@ -0,0 +1,2 @@ +public/app.js +public/dist diff --git a/todomvc/README.md b/todomvc/README.md new file mode 100644 index 0000000..807cc51 --- /dev/null +++ b/todomvc/README.md @@ -0,0 +1,23 @@ +# Newt "Spruce" framework • [TodoMVC](http://todomvc.com) + +## Implementation + +An implementation of TodoMVC, using the official CSS and DOM structure. I threw together an Elm-like framework that I'm calling "spruce" for now. It's lacking features, but works. + +I don't have a good story for javascript imports yet, so I'm embedding the DOM patcher in [Spruce.newt](../src/Web/Spruce.newt). + +## Dev + +If you background vite, you can rerun the newt command and reload the browser. Hot reload is not implemented. + +``` +newt src/Todo.newt -o public/app.js +vite public +``` + +## Build + +``` +newt src/Todo.newt -o public/app.js +vite build --minify public +``` diff --git a/todomvc/css-license.txt b/todomvc/css-license.txt new file mode 100644 index 0000000..2d6ec36 --- /dev/null +++ b/todomvc/css-license.txt @@ -0,0 +1,397 @@ +Creative Commons Attribution 4.0 International (CC-BY-4.0) + +Copyright (c) Sindre Sorhus (sindresorhus.com) + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/todomvc/public/index.html b/todomvc/public/index.html new file mode 100644 index 0000000..ac4223b --- /dev/null +++ b/todomvc/public/index.html @@ -0,0 +1,13 @@ + + + + + + Newt • TodoMVC + + + +
+ + + diff --git a/todomvc/public/style.css b/todomvc/public/style.css new file mode 100644 index 0000000..2c0b4b6 --- /dev/null +++ b/todomvc/public/style.css @@ -0,0 +1,393 @@ +@charset 'utf-8'; + +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #111111; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp h1 { + position: absolute; + top: -140px; + width: 100%; + font-size: 80px; + font-weight: 200; + text-align: center; + color: #b83f45; + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + height: 65px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 1px; + height: 1px; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; +} + +.toggle-all + label { + display: flex; + align-items: center; + justify-content: center; + width: 45px; + height: 65px; + font-size: 0; + position: absolute; + top: -65px; + left: -0; +} + +.toggle-all + label:before { + content: '❯'; + display: inline-block; + font-size: 22px; + color: #949494; + padding: 10px 27px 10px 27px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all:checked + label:before { + color: #484848; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: calc(100% - 43px); + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E'); +} + +.todo-list li label { + overflow-wrap: break-word; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; + font-weight: 400; + color: #484848; +} + +.todo-list li.completed label { + color: #949494; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #949494; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover, +.todo-list li .destroy:focus { + color: #C18585; +} + +.todo-list li .destroy:after { + content: '×'; + display: block; + height: 100%; + line-height: 1.1; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + padding: 10px 15px; + height: 20px; + text-align: center; + font-size: 15px; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: #DB7676; +} + +.filters li a.selected { + border-color: #CE4646; +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 19px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #4d4d4d; + font-size: 11px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} + +:focus, +.toggle:focus + label, +.toggle-all:focus + label { + box-shadow: 0 0 2px 2px #CF7D7D; + outline: 0; +} diff --git a/web/src/Prelude.newt b/todomvc/src/Prelude.newt similarity index 100% rename from web/src/Prelude.newt rename to todomvc/src/Prelude.newt diff --git a/todomvc/src/Todo.newt b/todomvc/src/Todo.newt new file mode 100644 index 0000000..77eb057 --- /dev/null +++ b/todomvc/src/Todo.newt @@ -0,0 +1,190 @@ +module Todo + +import Prelude +import Web.Spruce + +data FilterState = All | Active | Completed +derive Eq FilterState +data Msg + = Toggle Nat + | Remove Nat + -- TODO behavior varies by implementation, but probably should do return / esc / blur + | StartEdit Nat + | EndEdit Nat String + | Change String + | Filter FilterState + | Clear + | ToggleAll + +record Item where + checked : Bool + text : String + +data EditState = NoEdit | Edit Nat + +record Model where + items : List Item + newText : String + filter : FilterState + edit : EditState + +ElCon : U +ElCon = List (Attr Msg) → List (VNode Msg) → VNode Msg + +-- Attributes + +onChange : (String → Msg) → Attr Msg +onChange = VAttr "onChange" + +onClick : Msg → Attr Msg +onClick action = MAttr "OnClick" action + +className : String → Attr Msg +className v = SAttr "class" v + +checkbox : Bool → Nat → VNode Msg +checkbox checked ix = + tag "input" [ className "toggle" + , SAttr "checked" (ite checked "true" "") + , onChange (\ _ => Toggle ix) + , SAttr "type" "checkbox" ] [] + +-- Elements + +section : ElCon +section = tag "section" + +header : ElCon +header = tag "header" + +footer : ElCon +footer = tag "footer" + +div : ElCon +div = tag "div" + +ul : ElCon +ul = tag "ul" + +li : ElCon +li = tag "li" + +span : ElCon +span = tag "span" + +id_ : String → Attr Msg +id_ id = SAttr "id" id + +itemView : FilterState → EditState → Nat × Item → Maybe (VNode Msg) +itemView filter estate (ix , item) = + if exclude filter item then Nothing else + Just $ li [ className (getCName estate) ] + [ div [ className "view", MAttr "onDblClick" (StartEdit ix) ] + [ checkbox item.checked ix + , tag "label" [] [text item.text] + , tag "button" [ className "destroy", MAttr "onClick" (Remove ix)] [] + ] + , tag "input" [ className "edit" + , onChange (EndEdit ix) + , SAttr "value" item.text ] [ ] + ] + where + getCName : EditState → String + getCName NoEdit = ite item.checked "completed" "" + getCName (Edit n) = if n == ix then "editing" else if item.checked then "completed" else "" + + exclude : FilterState → Item → Bool + exclude All _ = False + exclude Active (MkItem checked text) = checked + exclude Completed (MkItem checked text) = not checked + +footerView : Model → VNode Msg +-- we don't have undefined/empty at the moment.. +footerView (MkModel Nil _ _ _) = div [] [] +footerView (MkModel items _ select _) = + let count = length' $ filter (\ item => not item.checked) items + ccount = length' $ filter (\ item => item.checked) items + label = text $ if count == 1 then " item left" else " items left" + in footer [className "footer"] + [ span [ className "todo-count" ] [ tag "strong" [] [ text $ show count ], label ] + , ul [ className "filters" ] + [ li [] [ tag "a" [ getClass All, onClick (Filter All)] [ text "All" ]] + , li [] [ tag "a" [ getClass Active, onClick (Filter Active)] [ text "Active" ]] + , li [] [ tag "a" [ getClass Completed, onClick (Filter Completed)] [ text "Completed" ]] + ] + , (if ccount == 0 then div [] [] else tag "button" [ className "clear-completed", onClick Clear ] [ text "Clear completed" ]) + ] + where + getClass : FilterState → Attr Msg + getClass x = if x == select then className "selected" else className "" + +listView : Model → VNode Msg +listView (MkModel Nil _ _ _) = div [] [] +listView (MkModel items newText select estate) = + let count = length' items + acount = length' $ (filter (\ item => not item.checked) items) + in tag "section" [ className "main" ] + [ tag "input" [ id_ "toggle-all" + , className "toggle-all" + , onClick ToggleAll + , SAttr "type" "checkbox"] [] + -- accessibility + , tag "label" [ SAttr "for" "toggle-all"] [ text "Mark all as complete" ] + , ul [ className "todo-list" ] (mapMaybe (itemView select estate) (enumerate items)) + ] +view : Model → VNode Msg +view model = + section [ className "todoapp" ] + [ header [ className "header" ] + [ tag "h1" [] [ text "todos" ] + , tag "input" [ className "new-todo" + , SAttr "placeholder" "What needs to be done?" + , SAttr "value" model.newText + , VAttr "onChange" Change + , SAttr "autofocus" "true"] []] + , listView model + , footerView model + ] + +isEmpty : ∀ a. List a → Bool +isEmpty Nil = True +isEmpty _ = False + +update : Msg → Model → Model +update Clear model = { items $= filter (\ item => not item.checked) } model +update (StartEdit ix) model = { edit := Edit ix } model +update (EndEdit ix text) model = { edit := NoEdit; items $= edit ix } model + where + edit : Nat → List Item → List Item + edit Z (item :: items) = the Item { text := text } item :: items + edit (S k) (item :: items) = item :: edit k items + edit _ Nil = Nil + +update ToggleAll model = + let checked = not $ isEmpty $ filter (\item => not item.checked) model.items + in { items $= map (the (Item → Item) { checked := checked }) } model + +update (Toggle ix) model = { items $= toggle ix } model + where + toggleItem : Item → Item + toggleItem item = { checked := not item.checked } item + + toggle : Nat → List Item → List Item + toggle _ Nil = Nil + toggle Z (item :: rest) = toggleItem item :: rest + toggle (S k) (item :: rest) = item :: toggle k rest + +update (Filter filter) model = { filter := filter } model +update (Remove ix) model = {items $= delete ix } model + where + delete : Nat → List Item → List Item + delete _ Nil = Nil + delete Z (item :: rest) = rest + delete (S k) (item :: rest) = item :: delete k rest + +update (Change text) model = { items := (snoc model.items $ MkItem False text) } model + +main : IO Unit +main = pure $ runApp (getElementById "main") (MkModel [] "" All NoEdit) update view + + diff --git a/todomvc/src/Web b/todomvc/src/Web new file mode 120000 index 0000000..86b644b --- /dev/null +++ b/todomvc/src/Web @@ -0,0 +1 @@ +../../src/Web \ No newline at end of file diff --git a/web/.gitignore b/web/.gitignore deleted file mode 100644 index cf66596..0000000 --- a/web/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dist/main.js diff --git a/web/README.md b/web/README.md deleted file mode 100644 index ce1a014..0000000 --- a/web/README.md +++ /dev/null @@ -1,12 +0,0 @@ - - -# Web framework experiment - -Work in progress of a DOM-patching web framework, like Elm. - -In the root, you can do: - -```sh -newt src/Todo.newt -o dist/main.js -esbuild dist/main.js --bundle --servedir=web --outdir=web --alias:fs=./dist/empty.js --watch -``` diff --git a/web/dist/empty.js b/web/dist/empty.js deleted file mode 100644 index b1c6ea4..0000000 --- a/web/dist/empty.js +++ /dev/null @@ -1 +0,0 @@ -export default {} diff --git a/web/dist/index.html b/web/dist/index.html deleted file mode 100644 index bafd1bc..0000000 --- a/web/dist/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - Counter - - - -
- - - diff --git a/web/dist/spruce.ts b/web/dist/spruce.ts deleted file mode 100644 index 15725e9..0000000 --- a/web/dist/spruce.ts +++ /dev/null @@ -1,126 +0,0 @@ -// We're going to do the VDom thing, directly off of newt data - -type Attrib = - | { tag: 0; h1: string; h2: string } - | { tag: 1; h1: string; h2: Msg } - | { tag: 2; h1: string; h2: (_: string) => Msg }; - -type List = { tag: 0 } | { tag: 1; h1: A; h2: List }; - -type VNode = - | { tag: 0; h1: string } // TNode - | { tag: 1; h1: string; h2: List>; h3: List> }; - -type FancyElement = Element & { events?: Record > }; - -// the pfunc will have two dummy arguments in front, and I dunno no node? -export function runapp( - node: Node, - init: Model, - update: (_: Msg) => (_: Model) => Model, - view: (_: Model) => VNode, -) { - function replace(parent: Node, node: Node | undefined, child: Node) { - if (node) { - parent.insertBefore(child, node); - parent.removeChild(node); - } else { - parent.appendChild(child); - } - return child; - } - // patch node, possibly returning a new node - function patch( - parentNode: Node, - node: Node | undefined, - vnode: VNode, - ): Node { - if (vnode.tag == 0) { - if (node && node.nodeType == 3) { - node.nodeValue = vnode.h1; - return node; - } - return replace(parentNode, node, document.createTextNode(vnode.h1)); - } - let el: FancyElement; - if (node instanceof Element && node.tagName.toLowerCase() == vnode.h1) { - el = node; - } else { - el = document.createElement(vnode.h1); - } - // update node here - let has: Record = {}; - for (let attrs = vnode.h2; attrs.tag == 1; attrs = attrs.h2) { - let attr = attrs.h1; - if (attr.tag == 0) { - let key = attr.h1; - has[key] = true; - if (key in el) - (el as any)[key] = attr.h2 - else - el.setAttribute(key, attr.h2); - } else { - // onBlah - let key = attr.h1.slice(2).toLowerCase(); - has[key] = true; - let events = el.events || (el.events = {}); - if (!events[key]) el.addEventListener(key, listener); - events[key] = attr; - } - } - // remove attrs, not efficient.. - for (let i = 0; i < el.attributes.length; ) { - let attr = el.attributes[i]; - if (!has[attr.name]) { - el.removeAttribute(attr.name); - } else { - i++; - } - } - if (el.events) { - for (let key of Object.keys(el.events)) { - if (!has[key]) delete el.events[key]; - } - } - let i = 0; - for (let kids = vnode.h3; kids.tag == 1; kids = kids.h2) { - patch(el, el.childNodes[i++], kids.h1); - } - while (el.childNodes[i]) el.removeChild(el.childNodes[i]); - - return node == el ? node : replace(parentNode, node, el); - } - let model = init; - let vdom = view(model); - let listener = (ev: Event) => { - let target = ev.target as FancyElement; - if (!target.events) return; - const attr = target.events[ev.type] - let action - if (attr.tag === 2) { - // probably need to pass back the event - // we'll want to deal with keypress, change, ... - let action = attr.h2(ev.target!.value) - model = update(action)(model); - } else if (attr.tag === 1) { - model = update(attr.h2)(model); - } - vdom = view(model); - node = patch(node.parentNode!, node, vdom); - }; - node = patch(node.parentNode!, node, vdom); -} -[{ - "resource": "/Users/dunham/prj/newt/src/Web/spruce.ts", - "owner": "typescript", - "code": "2349", - "severity": 8, - "message": "This expression is not callable.\n Not all constituents of type '((_: string) => Msg) | (Msg & Function)' are callable.\n Type 'Msg & Function' has no call signatures.", - "source": "ts", - "startLineNumber": 101, - "startColumn": 16, - "endLineNumber": 101, - "endColumn": 19, - "modelVersionId": 289, - "origin": "extHost1" -}] diff --git a/web/dist/style.css b/web/dist/style.css deleted file mode 100644 index 4b53a64..0000000 --- a/web/dist/style.css +++ /dev/null @@ -1,13 +0,0 @@ -.item { - display: flex; -} -.item > * { - vertical-align: middle; - padding-right: 10px; -} -.item > div { - display: inline; - width: 100px; - overflow: hidden; - text-overflow: ellipsis; -} diff --git a/web/src/Spruce.newt b/web/src/Spruce.newt deleted file mode 100644 index a3a8134..0000000 --- a/web/src/Spruce.newt +++ /dev/null @@ -1,31 +0,0 @@ -module Spruce - --- Spruce is not Elm - -import Prelude - -ptype Element -pfunc getElementById : String → Element := `(id) => document.getElementById(id)` - --- Make this align with spruce.ts -data Attr msg = SAttr String String | MAttr String msg | VAttr String (String → msg) -data VNode msg = TNode String | ENode String (List $ Attr msg) (List $ VNode msg) - -text : ∀ msg. String → VNode msg -text = TNode - -tag : ∀ msg. String → List (Attr msg)→ List (VNode msg) → VNode msg -tag = ENode - -tag_ : ∀ msg. String → List (VNode msg)→ VNode msg -tag_ tag es = ENode tag Nil es - --- TODO better story for import.. --- also, we'll probably want to expose something with subscriptions / dispatch / ... -pfunc runApp : ∀ msg model. Element → model → (update : msg → model → model) → (view : model → VNode msg) → Unit := - `(_msg, _model, el, init, update, view) => { - require('./spruce').runapp(el,init,update,view); - return 0; - }` - --- App diff --git a/web/src/Todo.newt b/web/src/Todo.newt deleted file mode 100644 index d7dc7ce..0000000 --- a/web/src/Todo.newt +++ /dev/null @@ -1,84 +0,0 @@ -module Todo - -import Prelude -import Spruce - -data Msg - = Toggle Nat - | Remove Nat - | Change String - -record Item where - checked : Bool - text : String - -record Model where - items : List Item - edit : String - -ElCon : U -ElCon = List (Attr Msg) → List (VNode Msg) → VNode Msg - --- Attributes - -onChange : Msg → Attr Msg -onChange = MAttr "onChange" - -onClick : Msg → Attr Msg -onClick action = MAttr "OnClick" action - -className : String → Attr Msg -className v = SAttr "class" v - -checkbox : Bool → Nat → VNode Msg -checkbox checked ix = - tag "input" [ SAttr "checked" (ite checked "true" ""), onChange (Toggle ix), SAttr "type" "checkbox" ] [] - --- Elements - -div : ElCon -div = tag "div" - -button : String → Msg → VNode Msg -button label action = tag "button" [ onClick action ] [ text label ] - - -itemView : Nat × Item → VNode Msg -itemView (ix , item) = - div [ className "item" ] - [ checkbox item.checked ix - , div [] [text item.text] - , button "x" (Remove ix) - ] - -view : Model → VNode Msg -view model = - div [] - [ div [] (map itemView $ enumerate model.items) - , tag "input" [ SAttr "value" model.edit, VAttr "onChange" Change ] [] - ] - -update : Msg → Model → Model -update (Toggle ix) model = { items $= toggle ix } model - where - toggleItem : Item → Item - toggleItem item = { checked := not item.checked } item - - toggle : Nat → List Item → List Item - toggle _ Nil = Nil - toggle Z (item :: rest) = toggleItem item :: rest - toggle (S k) (item :: rest) = item :: toggle k rest - -update (Remove ix) model = {items $= delete ix } model - where - delete : Nat → List Item → List Item - delete _ Nil = Nil - delete Z (item :: rest) = rest - delete (S k) (item :: rest) = item :: delete k rest - -update (Change text) model = { items := (snoc model.items $ MkItem False text) } model - -main : IO Unit -main = pure $ runApp (getElementById "main") (MkModel [] "") update view - -