Getting there
This commit is contained in:
parent
c9f4266d34
commit
84fcd0aeed
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,4 +2,4 @@
|
|||||||
/webapp/dist/
|
/webapp/dist/
|
||||||
/.parcel-cache/
|
/.parcel-cache/
|
||||||
/.idea
|
/.idea
|
||||||
|
/notes
|
30
.vscode/noot.code-workspace
vendored
30
.vscode/noot.code-workspace
vendored
@ -1,13 +1,19 @@
|
|||||||
{
|
{
|
||||||
"folders": [
|
"folders": [
|
||||||
{
|
{
|
||||||
"path": ".."
|
"path": ".."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../server"
|
"path": "../server"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true,
|
||||||
}
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
}
|
"javascript.format.enable": false,
|
||||||
|
"typescript.format.enable": false,
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.tabSize": 2
|
"editor.tabSize": 2
|
||||||
}
|
}
|
||||||
|
@ -1 +1,2 @@
|
|||||||
Yo!!
|
Yo!!
|
||||||
|
|
||||||
|
145
notes/start.md
145
notes/start.md
@ -1,144 +1,3 @@
|
|||||||
# Great Parenting!
|
Home page
|
||||||
> “Zef, you keep talking about all these amazing management concepts. You are an inspiration to all. However, what I really want to know is how you apply your enlightened ideas to being a parent!” — Nobody, Ever
|
|
||||||
|
|
||||||
Dear mister or miss “Ever” — if this is in fact your real name. I’m so glad you asked!
|
[[Great Parenting]]
|
||||||
|
|
||||||
For the rest of you, if you are not a parent, have no plan to be, or have absolutely zero interest in my philosophy of parenting — the latter seems unlikely to me, but I want to call out anyway — feel free to stop reading.
|
|
||||||
|
|
||||||
“So, do I need your permission to stop reading now? I will decide that myself, thank you very much!”
|
|
||||||
|
|
||||||
Thank you for sharing that perspective. However, in your case specifically, I have to insist you keep reading. It’s kids with your independent mindset that we’re trying to raise here. Although, perhaps you already know how to do that. Get in touch.
|
|
||||||
|
|
||||||
|
|
||||||
Hello there
|
|
||||||
|
|
||||||
-----
|
|
||||||
|
|
||||||
Before we start, why would you even listen to me? Let’s [[be honest]] here, I have no relevant credentials beyond being a dad myself. However, has that ever stopped anybody from doing anything? As any conspiracy theorist would say: you don’t need to take my word for it, do your own research — I’m just asking questions!
|
|
||||||
|
|
||||||
That said, it’s worth knowing I have three kids. All boys. One eight-year old, and five-year-old twins.
|
|
||||||
|
|
||||||
I know what you’re thinking.
|
|
||||||
|
|
||||||
I’m a former academic, I’m not blind to it. My family’s twin boys situation makes for a great **A/B testing opportunity**. So let’s address that right off the bat.
|
|
||||||
|
|
||||||
With two boys of the same age (although one will gladly point out that he was pulled out 2 minutes earlier, so he’s the older one), we could apply one type of experimental parenting technique to one of the twins, and another to the second one.
|
|
||||||
|
|
||||||
Sadly, there are a few reasons that make this a no go:
|
|
||||||
|
|
||||||
1. **Moral:** we have fairly strong sense how we want to raise our kids, and I’m afraid that the argument “sorry, you were the control group” is not going to fly thirty years down the road — as the “B” kid inevitably work through his dysfunctions with his psychologist.
|
|
||||||
2. **Variance:** even without A/B test our twins are shockingly different. We try to apply a similar approach to parenting with all our kids, still the twins have extremely different behavior. It is unlikely we would be able to tell, thirty years down the line, which parts of their life are screwed up due to our parenting experiment, and which ones are due to how they already were when they popped out. Although, obviously, we will claim full credit for everything if they turn out successful.
|
|
||||||
3. **Statistical significance:** two boys is a tiny sample size. To get anywhere, we would need a larger sample. Maybe a hundred kids, ideally twin’ed up. My wife has vetoed this idea.
|
|
||||||
|
|
||||||
So no A/B testing with our kids. It is what it is.
|
|
||||||
|
|
||||||
Instead, we will just have to go with our gut here, or make an educated guess. And with that, let me put in a disclaimer: if you decide to take any of this seriously, it’s at your own risk. Don’t come to me in thirty years saying “wow man, we really screwed up my kid with your advice.” Likely I’ll be in the same boat.
|
|
||||||
|
|
||||||
Do your own research.
|
|
||||||
|
|
||||||
---- -
|
|
||||||
|
|
||||||
However, if you’re going to severely limit your own research and just have budgeted one book to read, I would recommend that book to be [Unconditional Parenting](https://amzn.to/3rWwLT4) by *Alfie Kohn*.
|
|
||||||
|
|
||||||
You may remember that I brought up Kohn’s work before. He’s also the guy who wrote [Punished By Rewards](https://amzn.to/3HcBeWd) (which I spoke about at length in [No More Rewards](https://zef.plus/no-more-rewards/)) and [No Contest](https://amzn.to/3LKlgFU) (which I still have to write up). He’s one of my heroes in many ways. He has contrarian ideas, backed up with so much data and research that you can only conclude: oh wow, how could we have been doing this wrong for so long?
|
|
||||||
|
|
||||||
If you have [No More Rewards](https://zef.plus/no-more-rewards/) fresh in your head (as you should), you will actually not be surprised by the parenting approach proposed by Kohn at all. This is to be expected, because, spoiler alert: kids are people too.
|
|
||||||
|
|
||||||
I believe that a lot of challenges people face later in life can be traced back to how they were raised. Everything can be explained by trauma. Read [The Laws of Human Nature](https://amzn.to/3uZIdPz), and you will learn that for a huge amount of “dysfunctions” in people can be traced back to what their context looked like in their early years. Which, incidentally, is another reason to put at least some effort into this whole parenting topic. At least finish this post, is that too much to ask?
|
|
||||||
|
|
||||||
Enough prelude.
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
So, what does Kohn suggest? The way I would frame it (although Kohn does not) is to *work backwards.* Indeed, very Amazonian. Think about what we like our kids to be like when they grow up, and then create a context for them today in which is it is likely they get there.
|
|
||||||
|
|
||||||
What should our kids be like when they’re teenagers, in their twenties and beyond? For many of us, things that will jump to mind will include that they should be **independent**, **creative**, and **confident**. And rich, of course, so they pay us back all that money we invested in them. Do you have any idea what I cost per hour? With interest and taking into account inflation please.
|
|
||||||
|
|
||||||
Great. So let’s see what we can do *now* to help them get there.
|
|
||||||
|
|
||||||
Let’s start with a case study.
|
|
||||||
|
|
||||||
![](IMG_0087.jpeg)
|
|
||||||
|
|
||||||
Meet Leo (picture from a few years back). Leo is the 2-minute-younger twin. Cute, I know.
|
|
||||||
|
|
||||||
What is he doing? He’s showing us a drawing he made.
|
|
||||||
|
|
||||||
How do we respond to this masterpiece?
|
|
||||||
|
|
||||||
What most of us intuitively would do is is say something along the lines of “That’s beautiful!” “Great job!” “Let’s put that on the fridge!”
|
|
||||||
|
|
||||||
We had mentioned that we want our kids to grow up **confident** so this strategy seems to make sense. We help Leo to feel confident by praising his work. You’re awesome, because you drew an awesome bear (I think that’s a bear, at least).
|
|
||||||
|
|
||||||
However, let’s take a step back. Why do you think Leo is showing us this picture? Is he seeking praise? Approval? Does he need *us* (his parents) to feel confident about himself?
|
|
||||||
|
|
||||||
Leo is comfortable. He (rightfully) believes he is loved, and whatever he does won’t change that. He’s a kid, motivated to explore the world and to master his skills. As he progresses, he enjoys sharing this progress with the most important people in his life: us (for now). He can judge himself that he’s making progress (the bear from a month ago looked more like an elephant), he is confident, he has no reason not to be.
|
|
||||||
|
|
||||||
But then, we say “good job, that’s a beautiful drawing!” We offer to put the drawing on the fridge. He may get an ice cream, because we love the drawing that much.
|
|
||||||
|
|
||||||
We’re now changing the game.
|
|
||||||
|
|
||||||
Whereas Leo was purely *intrinsically motivated* before (and **self**-confident as a result), we’ve now (accidentally) taken a step to *extrinsically motivate* him. We have taken a (small) step to make his confidence dependent on external factors: you’re awesome, **because** you drew a bear that was qualified by your parents as “awesome.” He now starts to learn that the world is judging him, and that he should care what people think (it’s coming from his parents after all). And… he’s liking it, because he gets smiles, cheers and ice cream.
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
Let’s recap this whole [intrinsic vs. extrinsic motivation](https://zef.plus/no-more-rewards/) topic we spoke about in the past.
|
|
||||||
|
|
||||||
[Self-determination theory](https://en.wikipedia.org/wiki/Self-determination_theory) says there are two types of motivation with very different properties:
|
|
||||||
|
|
||||||
1. **Intrinsic motivation** is the natural, inherent drive to seek out challenges and new possibilities. Intrinsic motivation is present naturally. It does not need constant reinforcement, it persists unless actively pushed out.
|
|
||||||
2. **Extrinsic motivation** comes from external sources, for kids this can come in the shape of praise, stickers, ice cream. Compared to intrinsic motivation, extrinsic motivation is easier to control from the outside (hence extrinsic), but it is also short lived — it requires constant *reinforcement*.
|
|
||||||
|
|
||||||
Let this be one of few cases where I express clear judgment: *intrinsic* motivation is the good kind, that’s what we want. In our kids; in the people we work with. Extrinsic motivation is *convenient* (we can easily use it to get people to do what we want), but it’s not long-term sustainable.
|
|
||||||
|
|
||||||
Intrinsic motivation comes from three basic desires that are just as applicable to kids as they are to adults:
|
|
||||||
|
|
||||||
1. **Autonomy** — our ability to have control our own acts and life.
|
|
||||||
2. **Competence** — our need to control outcomes and experience mastery.
|
|
||||||
3. **Relatedness** our will to interact with, be connected to others (a sense *belonging* and *purpose*).
|
|
||||||
|
|
||||||
Growing up is all about these things: gaining *autonomy* and *competence* by being able to do more and more things yourself (eating, walking, drawing, writing, reading), all the while being *connected* with the people most important to us (our parents).
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
By praising Leo we are not only at risk replacing intrinsic motivation with extrinsic motivation, we are also reducing his autonomy. Leo’s sense of self worth and confidence becomes a bit more **dependent** on what we (his parents) think. For now he still enjoys drawing, but he starts to be worried whether his parents will like *this* drawing just as much or more than the *previous* one. He liked getting that praise, and seeing all his drawings on the fridge feels good. He wants more of that. It’s kind of addictive. To ensure another reward, he’s likely to play it safe and draw something similar (bye bye **creativity** — which is all about feeling the space to take risk without repercussions).
|
|
||||||
|
|
||||||
Leo makes another drawing. A bit quicker this time, with less effort. The quality is worse (a [known issue with extrinsically motivated work](https://zef.plus/no-more-rewards/)). We don’t respond as enthusiastically as before. Did he do something wrong? Do his parents still love him just as much?
|
|
||||||
|
|
||||||
This doesn’t seem like a great strategy working towards independent, creative, confident children.
|
|
||||||
|
|
||||||
Let’s try a different strategy. A strategy not dependent on rewards.
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
“Oh, you drew a bear and used multiple colors this time! Show me how you did that!”
|
|
||||||
|
|
||||||
We sit with Leo as he shows off his box of crayons, and offer suggestions on how to use the colors to make the next bear look even less like an elephant.
|
|
||||||
|
|
||||||
Absolutely, it is higher effort and time consuming than clamoring “great drawing, here’s an ice cream.” However, Leo still feels important because we spend time with him (boosting his confidence). Also, we don’t make him dependent on our judgment, nor block his creativity. Great!
|
|
||||||
|
|
||||||
Some other scenarios where praise would be an obvious response, but we approach things differently:
|
|
||||||
|
|
||||||
When our older son proudly shared he finished his first *Harry Potter* book (he has read it both in Polish and Dutch, not to brag or anything), we didn’t praise his reading skills. We didn’t reward him for it. Kohn, in his *Punished By Rewards* books cites a piece of research where they tried to “incentivize” reading by giving school kids money rewards for every book they read. The result: they read a lot more… thin books with lots of pictures, with very low recall of what they were about. The kids stopped reading the second the rewards stopped. So it worked, kinda, I suppose? But not really.
|
|
||||||
|
|
||||||
So we don’t do that. When my son fishes a book, we say that we can tell he really enjoys reading, and is getting better at it (mastery), why this matters (purpose), and then buy him the next *Harry Potter* book. Not as a reward, but as a means to support his mastery and joy of reading.
|
|
||||||
|
|
||||||
It’s a struggle. It often feels weird. But we feel it’s right.
|
|
||||||
|
|
||||||
----
|
|
||||||
|
|
||||||
Let’s put this all in perspective. Does this mean that if you say “good job!” you will turn your kids into compliant zombies that will no longer do anything unless they get a sticker? While Kohn describes one case in his book where kids were “nudged” with external motivators to such a level that they lost all sense of self, this is very extreme.
|
|
||||||
|
|
||||||
Chances are high that you, like most of us, have received your fair share of praise and sticker equivalents growing up. And you turned out alright. For some definition of alright, anyway.
|
|
||||||
|
|
||||||
However, if I were to go out on a limb, I could [ascribe events like the 2008 financial crisis](https://zef.plus/no-more-rewards/) to enough parents and teachers using their gold stars to motivate their kids, training them to be driven by extrinsic motivators like *bonuses* later in life. Could that be a big contributing factor? Did gold stars in school, and parents cheering “nice drawing” result in a financial crisis? I can’t prove that. I’m just asking questions.
|
|
||||||
|
|
||||||
However, what is becoming very visible is that our world is getting more challenging in this sense. For “kids these days” so [much of their self worth](https://www.netflix.com/title/81254224) is determined by how many *likes* they get on facebook. Kids are hammered by social platforms with external motivators. They post a selfie, and stressfully monitor how many hearts they get compared to their friends, and how many comments. How many followers do they have?
|
|
||||||
|
|
||||||
If we can do anything to equip our kids to be resistant to this type of stuff in the future, I’m eager to do it.
|
|
||||||
|
|
||||||
I’m not so naive to believe that I’ve now completely convinced you ought to completely change your approach to parenting tomorrow. Next week is fine too. After that your kids will be doomed though. Just saying.
|
|
||||||
|
|
||||||
Somewhat more seriously, go do your own research… by reading that [Unconditional Parenting](https://amzn.to/3rWwLT4) book.
|
|
||||||
|
|
||||||
No more rewards. Even for our kids.
|
|
128
server/server.ts
128
server/server.ts
@ -4,81 +4,91 @@ import FileInfo = Deno.FileInfo;
|
|||||||
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
|
import { Application, Router } from "https://deno.land/x/oak/mod.ts";
|
||||||
import { oakCors } from "https://deno.land/x/cors@v1.2.0/mod.ts";
|
import { oakCors } from "https://deno.land/x/cors@v1.2.0/mod.ts";
|
||||||
import { readAll } from "https://deno.land/std@0.126.0/streams/mod.ts";
|
import { readAll } from "https://deno.land/std@0.126.0/streams/mod.ts";
|
||||||
|
import { exists } from "https://deno.land/std@0.126.0/fs/mod.ts";
|
||||||
|
|
||||||
|
const fsPrefix = "/fs";
|
||||||
const fsPrefix = '/fs';
|
const notesPath = "../notes";
|
||||||
const notesPath = '../notes';
|
|
||||||
|
|
||||||
const fsRouter = new Router();
|
const fsRouter = new Router();
|
||||||
|
|
||||||
fsRouter.use(oakCors());
|
fsRouter.use(oakCors());
|
||||||
|
|
||||||
fsRouter.get('/', async context => {
|
fsRouter.get("/", async (context) => {
|
||||||
const localPath = notesPath;
|
const localPath = notesPath;
|
||||||
let fileNames: string[] = [];
|
let fileNames: string[] = [];
|
||||||
for await (const dirEntry of Deno.readDir(localPath)) {
|
for await (const dirEntry of Deno.readDir(localPath)) {
|
||||||
if (dirEntry.isFile) {
|
if (dirEntry.isFile) {
|
||||||
fileNames.push(dirEntry.name.substring(0, dirEntry.name.length - path.extname(dirEntry.name).length));
|
fileNames.push(
|
||||||
}
|
dirEntry.name.substring(
|
||||||
|
0,
|
||||||
|
dirEntry.name.length - path.extname(dirEntry.name).length
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
context.response.body = JSON.stringify(fileNames);
|
}
|
||||||
|
context.response.body = JSON.stringify(fileNames);
|
||||||
});
|
});
|
||||||
|
|
||||||
fsRouter.get('/:note', async context => {
|
fsRouter.get("/:note", async (context) => {
|
||||||
const noteName = context.params.note;
|
const noteName = context.params.note;
|
||||||
const localPath = `${notesPath}/${noteName}.md`;
|
const localPath = `${notesPath}/${noteName}.md`;
|
||||||
try {
|
try {
|
||||||
const text = await Deno.readTextFile(localPath);
|
const text = await Deno.readTextFile(localPath);
|
||||||
context.response.body = text;
|
context.response.body = text;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
context.response.status = 404;
|
context.response.status = 404;
|
||||||
context.response.body = "";
|
context.response.body = "";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
fsRouter.options('/:note', async context => {
|
fsRouter.options("/:note", async (context) => {
|
||||||
const localPath = `${notesPath}/${context.params.note}.md`;
|
const localPath = `${notesPath}/${context.params.note}.md`;
|
||||||
try {
|
try {
|
||||||
const stat = await Deno.stat(localPath);
|
const stat = await Deno.stat(localPath);
|
||||||
context.response.headers.set('Content-length', `${stat.size}`);
|
context.response.headers.set("Content-length", `${stat.size}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
context.response.status = 200;
|
context.response.status = 200;
|
||||||
context.response.body = "";
|
context.response.body = "";
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
fsRouter.put('/:note', async context => {
|
fsRouter.put("/:note", async (context) => {
|
||||||
const noteName = context.params.note;
|
const noteName = context.params.note;
|
||||||
|
const localPath = `${notesPath}/${noteName}.md`;
|
||||||
const localPath = `${notesPath}/${noteName}.md`;
|
const existingNote = await exists(localPath);
|
||||||
let file;
|
let file;
|
||||||
try {
|
try {
|
||||||
file = await Deno.create(localPath);
|
file = await Deno.create(localPath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error opening file for writing", localPath, e);
|
console.error("Error opening file for writing", localPath, e);
|
||||||
context.response.status = 500;
|
context.response.status = 500;
|
||||||
context.response.body = e.message;
|
context.response.body = e.message;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = context.request.body({ type: "reader" });
|
const result = context.request.body({ type: "reader" });
|
||||||
const text = await readAll(result.value);
|
const text = await readAll(result.value);
|
||||||
file.write(text);
|
file.write(text);
|
||||||
file.close();
|
file.close();
|
||||||
console.log("Wrote to", localPath)
|
console.log("Wrote to", localPath);
|
||||||
context.response.body = "OK";
|
context.response.status = existingNote ? 200 : 201;
|
||||||
|
context.response.body = "OK";
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = new Application();
|
const app = new Application();
|
||||||
app.use(new Router().use(fsPrefix, fsRouter.routes(), fsRouter.allowedMethods()).routes());
|
app.use(
|
||||||
|
new Router()
|
||||||
|
.use(fsPrefix, fsRouter.routes(), fsRouter.allowedMethods())
|
||||||
|
.routes()
|
||||||
|
);
|
||||||
app.use(async (context, next) => {
|
app.use(async (context, next) => {
|
||||||
try {
|
try {
|
||||||
await context.send({
|
await context.send({
|
||||||
root: '../webapp/dist',
|
root: "../webapp/dist",
|
||||||
index: 'index.html'
|
index: "index.html",
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await app.listen({ port: 2222 });
|
await app.listen({ port: 2222 });
|
||||||
|
2
server/test.ts
Normal file
2
server/test.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { parser } from "https://unpkg.com/@lezer/markdown?module";
|
||||||
|
console.log(parser);
|
@ -6,7 +6,8 @@
|
|||||||
"browserslist": "> 0.5%, last 2 versions, not dead",
|
"browserslist": "> 0.5%, last 2 versions, not dead",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "parcel",
|
"start": "parcel",
|
||||||
"build": "parcel build"
|
"build": "parcel build",
|
||||||
|
"check-watch": "tsc --noEmit --watch"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@parcel/validator-typescript": "^2.3.2",
|
"@parcel/validator-typescript": "^2.3.2",
|
||||||
@ -21,7 +22,6 @@
|
|||||||
"@codemirror/lang-markdown": "^0.19.6",
|
"@codemirror/lang-markdown": "^0.19.6",
|
||||||
"@codemirror/state": "^0.19.7",
|
"@codemirror/state": "^0.19.7",
|
||||||
"@codemirror/view": "^0.19.42",
|
"@codemirror/view": "^0.19.42",
|
||||||
"kbar": "^0.1.0-beta.27",
|
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2"
|
"react-dom": "^17.0.2"
|
||||||
}
|
}
|
||||||
|
@ -16,23 +16,26 @@ import {
|
|||||||
import React, { useEffect, useReducer, useRef } from "react";
|
import React, { useEffect, useReducer, useRef } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import * as commands from "./commands";
|
import * as commands from "./commands";
|
||||||
|
import { CommandPalette } from "./components/commandpalette";
|
||||||
|
import { NoteNavigator } from "./components/notenavigator";
|
||||||
import { HttpFileSystem } from "./fs";
|
import { HttpFileSystem } from "./fs";
|
||||||
import { lineWrapper } from "./lineWrapper";
|
import { lineWrapper } from "./lineWrapper";
|
||||||
import { markdown } from "./markdown";
|
import { markdown } from "./markdown";
|
||||||
import customMarkDown from "./parser";
|
import customMarkDown from "./parser";
|
||||||
import customMarkdownStyle from "./style";
|
|
||||||
|
|
||||||
import { FilterList } from "./components/filter";
|
|
||||||
|
|
||||||
import { NoteMeta, AppViewState, Action } from "./types";
|
|
||||||
import reducer from "./reducer";
|
import reducer from "./reducer";
|
||||||
|
import customMarkdownStyle from "./style";
|
||||||
|
import { Action, AppViewState } from "./types";
|
||||||
|
|
||||||
|
import { syntaxTree } from "@codemirror/language";
|
||||||
|
import * as util from "./util";
|
||||||
|
|
||||||
const fs = new HttpFileSystem("http://localhost:2222/fs");
|
const fs = new HttpFileSystem("http://localhost:2222/fs");
|
||||||
|
|
||||||
const initialViewState = {
|
const initialViewState: AppViewState = {
|
||||||
currentNote: "",
|
currentNote: "",
|
||||||
isSaved: false,
|
isSaved: false,
|
||||||
isFiltering: false,
|
showNoteNavigator: false,
|
||||||
|
showCommandPalette: false,
|
||||||
allNotes: [],
|
allNotes: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -56,6 +59,7 @@ class Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createEditorState(text: string): EditorState {
|
createEditorState(text: string): EditorState {
|
||||||
|
const editor = this;
|
||||||
return EditorState.create({
|
return EditorState.create({
|
||||||
doc: text,
|
doc: text,
|
||||||
extensions: [
|
extensions: [
|
||||||
@ -107,6 +111,25 @@ class Editor {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "Ctrl-Enter",
|
||||||
|
mac: "Cmd-Enter",
|
||||||
|
run: (target): boolean => {
|
||||||
|
// TODO: Factor this and click handler into one action
|
||||||
|
let selection = target.state.selection.main;
|
||||||
|
if (selection.empty) {
|
||||||
|
let node = syntaxTree(target.state).resolveInner(
|
||||||
|
selection.from
|
||||||
|
);
|
||||||
|
if (node && node.name === "WikiLinkPage") {
|
||||||
|
let noteName = target.state.sliceDoc(node.from, node.to);
|
||||||
|
this.navigate(noteName);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "Ctrl-p",
|
key: "Ctrl-p",
|
||||||
mac: "Cmd-p",
|
mac: "Cmd-p",
|
||||||
@ -115,6 +138,14 @@ class Editor {
|
|||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "Ctrl-.",
|
||||||
|
mac: "Cmd-.",
|
||||||
|
run: (target): boolean => {
|
||||||
|
this.dispatch({ type: "show-palette" });
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
]),
|
]),
|
||||||
EditorView.domEventHandlers({
|
EditorView.domEventHandlers({
|
||||||
click: this.click.bind(this),
|
click: this.click.bind(this),
|
||||||
@ -133,7 +164,7 @@ class Editor {
|
|||||||
update(value: null, transaction: Transaction): null {
|
update(value: null, transaction: Transaction): null {
|
||||||
if (transaction.docChanged) {
|
if (transaction.docChanged) {
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
type: "updated",
|
type: "note-updated",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,16 +178,34 @@ class Editor {
|
|||||||
|
|
||||||
click(event: MouseEvent, view: EditorView) {
|
click(event: MouseEvent, view: EditorView) {
|
||||||
if (event.metaKey || event.ctrlKey) {
|
if (event.metaKey || event.ctrlKey) {
|
||||||
console.log("Navigate click");
|
let coords = view.posAtCoords(event)!;
|
||||||
let coords = view.posAtCoords(event);
|
let node = syntaxTree(view.state).resolveInner(coords);
|
||||||
console.log("Coords", view.state.doc.sliceString(coords!, coords! + 1));
|
if (node && node.name === "WikiLinkPage") {
|
||||||
|
let noteName = view.state.sliceDoc(node.from, node.to);
|
||||||
|
this.navigate(noteName);
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
await fs.writeNote(this.currentNote, this.view.state.sliceDoc());
|
const created = await fs.writeNote(
|
||||||
this.dispatch({ type: "saved" });
|
this.currentNote,
|
||||||
|
this.view.state.sliceDoc()
|
||||||
|
);
|
||||||
|
this.dispatch({ type: "note-saved" });
|
||||||
|
// If a new note was created, let's refresh the note list
|
||||||
|
if (created) {
|
||||||
|
await this.loadNoteList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadNoteList() {
|
||||||
|
let notesMeta = await fs.listNotes();
|
||||||
|
this.dispatch({
|
||||||
|
type: "notes-listed",
|
||||||
|
notes: notesMeta,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
@ -168,43 +217,38 @@ class Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function TopBar({
|
let editor: Editor | null;
|
||||||
|
|
||||||
|
function NavigationBar({
|
||||||
currentNote,
|
currentNote,
|
||||||
isSaved,
|
|
||||||
isFiltering,
|
|
||||||
allNotes,
|
|
||||||
onNavigate,
|
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
currentNote: string;
|
currentNote: string;
|
||||||
isSaved: boolean;
|
|
||||||
isFiltering: boolean;
|
|
||||||
allNotes: NoteMeta[];
|
|
||||||
onNavigate: (note: string | undefined) => void;
|
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div id="top">
|
<div id="top">
|
||||||
<div className="current-note" onClick={onClick}>
|
<div className="current-note" onClick={onClick}>
|
||||||
» {currentNote}
|
» {currentNote}
|
||||||
{isSaved ? "" : "*"}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isFiltering && (
|
|
||||||
<FilterList
|
|
||||||
initialText=""
|
|
||||||
options={allNotes}
|
|
||||||
onSelect={(opt) => {
|
|
||||||
console.log("Selected", opt);
|
|
||||||
onNavigate(opt?.name);
|
|
||||||
}}
|
|
||||||
></FilterList>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let editor: Editor | null;
|
function StatusBar({ isSaved }: { isSaved: boolean }) {
|
||||||
|
let wordCount = 0,
|
||||||
|
readingTime = 0;
|
||||||
|
if (editor) {
|
||||||
|
let text = editor.view.state.sliceDoc();
|
||||||
|
wordCount = util.countWords(text);
|
||||||
|
readingTime = util.readingTime(wordCount);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div id="bottom">
|
||||||
|
{wordCount} words | {readingTime} min | {isSaved ? "Saved" : "Edited"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function AppView() {
|
function AppView() {
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
@ -221,16 +265,21 @@ function AppView() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fs.listNotes()
|
editor?.loadNoteList();
|
||||||
.then((notes) => {
|
|
||||||
dispatch({
|
|
||||||
type: "notes-list",
|
|
||||||
notes: notes,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((e) => console.error(e));
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Auto save
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
if (!appState.isSaved) {
|
||||||
|
editor?.save();
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(id);
|
||||||
|
};
|
||||||
|
}, [appState.isSaved]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function hashChange() {
|
function hashChange() {
|
||||||
const noteName = decodeURIComponent(location.hash.substring(1));
|
const noteName = decodeURIComponent(location.hash.substring(1));
|
||||||
@ -240,7 +289,7 @@ function AppView() {
|
|||||||
.then((text) => {
|
.then((text) => {
|
||||||
editor!.load(noteName, text);
|
editor!.load(noteName, text);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: "loaded",
|
type: "note-loaded",
|
||||||
name: noteName,
|
name: noteName,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@ -257,24 +306,45 @@ function AppView() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TopBar
|
{appState.showNoteNavigator && (
|
||||||
|
<NoteNavigator
|
||||||
|
allNotes={appState.allNotes}
|
||||||
|
onNavigate={(note) => {
|
||||||
|
dispatch({ type: "stop-navigate" });
|
||||||
|
editor!.focus();
|
||||||
|
if (note) {
|
||||||
|
editor
|
||||||
|
?.save()
|
||||||
|
.then(() => {
|
||||||
|
editor!.navigate(note);
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
alert("Could not save note, not switching");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{appState.showCommandPalette && (
|
||||||
|
<CommandPalette
|
||||||
|
onTrigger={(cmd) => {
|
||||||
|
dispatch({ type: "hide-palette" });
|
||||||
|
editor!.focus();
|
||||||
|
if (cmd) {
|
||||||
|
console.log("Run", cmd);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
commands={[{ name: "My command", run: () => {} }]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<NavigationBar
|
||||||
currentNote={appState.currentNote}
|
currentNote={appState.currentNote}
|
||||||
isSaved={appState.isSaved}
|
|
||||||
isFiltering={appState.isFiltering}
|
|
||||||
allNotes={appState.allNotes}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
dispatch({ type: "start-navigate" });
|
dispatch({ type: "start-navigate" });
|
||||||
}}
|
}}
|
||||||
onNavigate={(note) => {
|
|
||||||
dispatch({ type: "stop-navigate" });
|
|
||||||
editor!.focus();
|
|
||||||
if (note) {
|
|
||||||
editor!.navigate(note);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<div id="editor" ref={editorRef}></div>
|
<div id="editor" ref={editorRef}></div>
|
||||||
<div id="bottom">Bottom</div>
|
<StatusBar isSaved={appState.isSaved} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,47 +1,60 @@
|
|||||||
import { EditorSelection, EditorState, StateCommand, Transaction } from "@codemirror/state";
|
import { EditorSelection, StateCommand, Transaction } from "@codemirror/state";
|
||||||
import { Text } from "@codemirror/text";
|
import { Text } from "@codemirror/text";
|
||||||
|
|
||||||
export function insertMarker(marker: string): StateCommand {
|
export function insertMarker(marker: string): StateCommand {
|
||||||
return ({ state, dispatch }) => {
|
return ({ state, dispatch }) => {
|
||||||
const changes = state.changeByRange((range) => {
|
const changes = state.changeByRange((range) => {
|
||||||
const isBoldBefore = state.sliceDoc(range.from - marker.length, range.from) === marker;
|
const isBoldBefore =
|
||||||
const isBoldAfter = state.sliceDoc(range.to, range.to + marker.length) === marker;
|
state.sliceDoc(range.from - marker.length, range.from) === marker;
|
||||||
const changes = [];
|
const isBoldAfter =
|
||||||
|
state.sliceDoc(range.to, range.to + marker.length) === marker;
|
||||||
|
const changes = [];
|
||||||
|
|
||||||
changes.push(isBoldBefore ? {
|
changes.push(
|
||||||
from: range.from - marker.length,
|
isBoldBefore
|
||||||
to: range.from,
|
? {
|
||||||
insert: Text.of([''])
|
from: range.from - marker.length,
|
||||||
} : {
|
to: range.from,
|
||||||
from: range.from,
|
insert: Text.of([""]),
|
||||||
insert: Text.of([marker]),
|
|
||||||
})
|
|
||||||
|
|
||||||
changes.push(isBoldAfter ? {
|
|
||||||
from: range.to,
|
|
||||||
to: range.to + marker.length,
|
|
||||||
insert: Text.of([''])
|
|
||||||
} : {
|
|
||||||
from: range.to,
|
|
||||||
insert: Text.of([marker]),
|
|
||||||
})
|
|
||||||
|
|
||||||
const extendBefore = isBoldBefore ? -marker.length : marker.length;
|
|
||||||
const extendAfter = isBoldAfter ? -marker.length : marker.length;
|
|
||||||
|
|
||||||
return {
|
|
||||||
changes,
|
|
||||||
range: EditorSelection.range(range.from + extendBefore, range.to + extendAfter),
|
|
||||||
}
|
}
|
||||||
})
|
: {
|
||||||
|
from: range.from,
|
||||||
|
insert: Text.of([marker]),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
dispatch(
|
changes.push(
|
||||||
state.update(changes, {
|
isBoldAfter
|
||||||
scrollIntoView: true,
|
? {
|
||||||
annotations: Transaction.userEvent.of('input'),
|
from: range.to,
|
||||||
})
|
to: range.to + marker.length,
|
||||||
)
|
insert: Text.of([""]),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
from: range.to,
|
||||||
|
insert: Text.of([marker]),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return true
|
const extendBefore = isBoldBefore ? -marker.length : marker.length;
|
||||||
};
|
const extendAfter = isBoldAfter ? -marker.length : marker.length;
|
||||||
}
|
|
||||||
|
return {
|
||||||
|
changes,
|
||||||
|
range: EditorSelection.range(
|
||||||
|
range.from + extendBefore,
|
||||||
|
range.to + extendAfter
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
state.update(changes, {
|
||||||
|
scrollIntoView: true,
|
||||||
|
annotations: Transaction.userEvent.of("input"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
21
webapp/src/components/commandpalette.tsx
Normal file
21
webapp/src/components/commandpalette.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { AppCommand } from "../types";
|
||||||
|
import { FilterList } from "./filter";
|
||||||
|
|
||||||
|
export function CommandPalette({
|
||||||
|
commands,
|
||||||
|
onTrigger,
|
||||||
|
}: {
|
||||||
|
commands: AppCommand[];
|
||||||
|
onTrigger: (command: AppCommand) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FilterList
|
||||||
|
placeholder="Enter command to run"
|
||||||
|
options={commands}
|
||||||
|
allowNew={false}
|
||||||
|
onSelect={(opt) => {
|
||||||
|
onTrigger(opt as AppCommand);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,41 +1,63 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
type Option = {
|
export interface Option {
|
||||||
name: string;
|
name: string;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function magicSorter(a: Option, b: Option): number {
|
||||||
|
if (a.name.toLowerCase() < b.name.toLowerCase()) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function FilterList({
|
export function FilterList({
|
||||||
initialText,
|
placeholder,
|
||||||
options,
|
options,
|
||||||
onSelect,
|
onSelect,
|
||||||
allowNew = false,
|
allowNew = false,
|
||||||
|
newHint,
|
||||||
}: {
|
}: {
|
||||||
initialText: string;
|
placeholder: string;
|
||||||
options: Option[];
|
options: Option[];
|
||||||
onSelect: (option: Option | undefined) => void;
|
onSelect: (option: Option | undefined) => void;
|
||||||
allowNew?: boolean;
|
allowNew?: boolean;
|
||||||
|
newHint?: string;
|
||||||
}) {
|
}) {
|
||||||
const searchBoxRef = useRef<HTMLInputElement>(null);
|
const searchBoxRef = useRef<HTMLInputElement>(null);
|
||||||
const [text, setText] = useState(initialText);
|
const [text, setText] = useState("");
|
||||||
const [matchingOptions, setMatchingOptions] = useState(options);
|
const [matchingOptions, setMatchingOptions] = useState(
|
||||||
|
options.sort(magicSorter)
|
||||||
|
);
|
||||||
const [selectedOption, setSelectionOption] = useState(0);
|
const [selectedOption, setSelectionOption] = useState(0);
|
||||||
|
|
||||||
|
let selectedElementRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const filter = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const filter = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const originalPhrase = e.target.value;
|
const originalPhrase = e.target.value;
|
||||||
const searchPhrase = originalPhrase.toLowerCase();
|
const searchPhrase = originalPhrase.toLowerCase();
|
||||||
|
|
||||||
if (searchPhrase) {
|
if (searchPhrase) {
|
||||||
|
let foundExactMatch = false;
|
||||||
let results = options.filter((option) => {
|
let results = options.filter((option) => {
|
||||||
|
if (option.name.toLowerCase() === searchPhrase) {
|
||||||
|
foundExactMatch = true;
|
||||||
|
}
|
||||||
return option.name.toLowerCase().indexOf(searchPhrase) !== -1;
|
return option.name.toLowerCase().indexOf(searchPhrase) !== -1;
|
||||||
});
|
});
|
||||||
results.splice(0, 0, {
|
results = results.sort(magicSorter);
|
||||||
name: originalPhrase,
|
if (allowNew && !foundExactMatch) {
|
||||||
hint: "Create new",
|
results.push({
|
||||||
});
|
name: originalPhrase,
|
||||||
|
hint: newHint,
|
||||||
|
});
|
||||||
|
}
|
||||||
setMatchingOptions(results);
|
setMatchingOptions(results);
|
||||||
} else {
|
} else {
|
||||||
setMatchingOptions(options);
|
let results = options.sort(magicSorter);
|
||||||
|
setMatchingOptions(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
setText(originalPhrase);
|
setText(originalPhrase);
|
||||||
@ -58,11 +80,12 @@ export function FilterList({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
const returEl = (
|
||||||
<div className="filter-container">
|
<div className="filter-container">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={text}
|
value={text}
|
||||||
|
placeholder={placeholder}
|
||||||
ref={searchBoxRef}
|
ref={searchBoxRef}
|
||||||
onChange={filter}
|
onChange={filter}
|
||||||
onKeyDown={(e: React.KeyboardEvent) => {
|
onKeyDown={(e: React.KeyboardEvent) => {
|
||||||
@ -86,7 +109,6 @@ export function FilterList({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="input"
|
className="input"
|
||||||
placeholder=""
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="result-list">
|
<div className="result-list">
|
||||||
@ -94,6 +116,7 @@ export function FilterList({
|
|||||||
? matchingOptions.map((option, idx) => (
|
? matchingOptions.map((option, idx) => (
|
||||||
<div
|
<div
|
||||||
key={"" + idx}
|
key={"" + idx}
|
||||||
|
ref={selectedOption === idx ? selectedElementRef : undefined}
|
||||||
className={
|
className={
|
||||||
selectedOption === idx ? "selected-option" : "option"
|
selectedOption === idx ? "selected-option" : "option"
|
||||||
}
|
}
|
||||||
@ -113,4 +136,12 @@ export function FilterList({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedElementRef.current?.scrollIntoView({
|
||||||
|
block: "nearest",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return returEl;
|
||||||
}
|
}
|
||||||
|
22
webapp/src/components/notenavigator.tsx
Normal file
22
webapp/src/components/notenavigator.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { NoteMeta } from "../types";
|
||||||
|
import { FilterList } from "./filter";
|
||||||
|
|
||||||
|
export function NoteNavigator({
|
||||||
|
allNotes,
|
||||||
|
onNavigate,
|
||||||
|
}: {
|
||||||
|
allNotes: NoteMeta[];
|
||||||
|
onNavigate: (note: string | undefined) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FilterList
|
||||||
|
placeholder=""
|
||||||
|
options={allNotes}
|
||||||
|
allowNew={true}
|
||||||
|
newHint="Create note"
|
||||||
|
onSelect={(opt) => {
|
||||||
|
onNavigate(opt?.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import { Tag } from '@codemirror/highlight';
|
import { Tag } from "@codemirror/highlight";
|
||||||
|
|
||||||
export const WikiLinkTag = Tag.define();
|
export const WikiLinkTag = Tag.define();
|
||||||
|
export const WikiLinkPageTag = Tag.define();
|
||||||
export const TagTag = Tag.define();
|
export const TagTag = Tag.define();
|
||||||
export const MentionTag = Tag.define();
|
export const MentionTag = Tag.define();
|
||||||
|
@ -1,37 +1,36 @@
|
|||||||
|
|
||||||
import { NoteMeta } from "./types";
|
import { NoteMeta } from "./types";
|
||||||
|
|
||||||
|
|
||||||
export interface FileSystem {
|
export interface FileSystem {
|
||||||
listNotes(): Promise<NoteMeta[]>;
|
listNotes(): Promise<NoteMeta[]>;
|
||||||
readNote(name: string): Promise<string>;
|
readNote(name: string): Promise<string>;
|
||||||
writeNote(name: string, text: string): Promise<void>;
|
// @return whether a new note was created for this
|
||||||
|
writeNote(name: string, text: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class HttpFileSystem implements FileSystem {
|
export class HttpFileSystem implements FileSystem {
|
||||||
url: string;
|
url: string;
|
||||||
|
constructor(url: string) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
async listNotes(): Promise<NoteMeta[]> {
|
||||||
|
let req = await fetch(this.url, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
constructor(url: string) {
|
return (await req.json()).map((name: string) => ({ name }));
|
||||||
this.url = url;
|
}
|
||||||
}
|
async readNote(name: string): Promise<string> {
|
||||||
async listNotes(): Promise<NoteMeta[]> {
|
let req = await fetch(`${this.url}/${name}`, {
|
||||||
let req = await fetch(this.url, {
|
method: "GET",
|
||||||
method: 'GET'
|
});
|
||||||
});
|
return await req.text();
|
||||||
|
}
|
||||||
return (await req.json()).map((name: string) => ({ name }));
|
async writeNote(name: string, text: string): Promise<boolean> {
|
||||||
}
|
let req = await fetch(`${this.url}/${name}`, {
|
||||||
async readNote(name: string): Promise<string> {
|
method: "PUT",
|
||||||
let req = await fetch(`${this.url}/${name}`, {
|
body: text,
|
||||||
method: 'GET'
|
});
|
||||||
});
|
// 201 (Created) means a new note was created
|
||||||
return await req.text();
|
return req.status === 201;
|
||||||
}
|
}
|
||||||
async writeNote(name: string, text: string): Promise<void> {
|
}
|
||||||
let req = await fetch(`${this.url}/${name}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: text
|
|
||||||
});
|
|
||||||
await req.text();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,56 +1,68 @@
|
|||||||
import { syntaxTree } from '@codemirror/language';
|
import { syntaxTree } from "@codemirror/language";
|
||||||
import {
|
import {
|
||||||
Decoration,
|
Decoration,
|
||||||
DecorationSet, EditorView, ViewPlugin,
|
DecorationSet,
|
||||||
ViewUpdate
|
EditorView,
|
||||||
} from '@codemirror/view';
|
ViewPlugin,
|
||||||
|
ViewUpdate,
|
||||||
|
} from "@codemirror/view";
|
||||||
|
|
||||||
import { Range } from '@codemirror/rangeset';
|
import { Range } from "@codemirror/rangeset";
|
||||||
|
|
||||||
interface WrapElement {
|
interface WrapElement {
|
||||||
selector: string;
|
selector: string;
|
||||||
class: string;
|
class: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
|
function wrapLines(view: EditorView, wrapElements: WrapElement[]) {
|
||||||
let widgets: Range<Decoration>[] = [];
|
let widgets: Range<Decoration>[] = [];
|
||||||
for (let { from, to } of view.visibleRanges) {
|
for (let { from, to } of view.visibleRanges) {
|
||||||
const doc = view.state.doc;
|
const doc = view.state.doc;
|
||||||
syntaxTree(view.state).iterate({
|
syntaxTree(view.state).iterate({
|
||||||
from, to,
|
from,
|
||||||
enter: (type, from, to) => {
|
to,
|
||||||
const bodyText = doc.sliceString(from, to);
|
enter: (type, from, to) => {
|
||||||
for (let wrapElement of wrapElements) {
|
const bodyText = doc.sliceString(from, to);
|
||||||
if (type.name == wrapElement.selector) {
|
for (let wrapElement of wrapElements) {
|
||||||
const bodyText = doc.sliceString(from, to);
|
if (type.name == wrapElement.selector) {
|
||||||
let idx = from;
|
const bodyText = doc.sliceString(from, to);
|
||||||
for (let line of bodyText.split("\n")) {
|
let idx = from;
|
||||||
widgets.push(Decoration.line({
|
for (let line of bodyText.split("\n")) {
|
||||||
class: wrapElement.class,
|
widgets.push(
|
||||||
}).range(doc.lineAt(idx).from));
|
Decoration.line({
|
||||||
idx += line.length + 1;
|
class: wrapElement.class,
|
||||||
}
|
}).range(doc.lineAt(idx).from)
|
||||||
}
|
);
|
||||||
}
|
idx += line.length + 1;
|
||||||
},
|
|
||||||
leave(type, from: number, to: number) {
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
|
||||||
// console.log("All widgets", widgets);
|
|
||||||
return Decoration.set(widgets);
|
|
||||||
}
|
|
||||||
export const lineWrapper = (wrapElements: WrapElement[]) => ViewPlugin.fromClass(class {
|
|
||||||
decorations: DecorationSet;
|
|
||||||
|
|
||||||
constructor(view: EditorView) {
|
|
||||||
this.decorations = wrapLines(view, wrapElements);
|
|
||||||
}
|
|
||||||
|
|
||||||
update(update: ViewUpdate) {
|
|
||||||
if (update.docChanged || update.viewportChanged) {
|
|
||||||
this.decorations = wrapLines(update.view, wrapElements);
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
leave(type, from: number, to: number) {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Widgets have to be sorted by `from` in ascending order
|
||||||
|
widgets = widgets.sort((a, b) => {
|
||||||
|
return a.from < b.from ? -1 : 1;
|
||||||
|
});
|
||||||
|
return Decoration.set(widgets);
|
||||||
|
}
|
||||||
|
export const lineWrapper = (wrapElements: WrapElement[]) =>
|
||||||
|
ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
decorations: DecorationSet;
|
||||||
|
|
||||||
|
constructor(view: EditorView) {
|
||||||
|
this.decorations = wrapLines(view, wrapElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(update: ViewUpdate) {
|
||||||
|
if (update.docChanged || update.viewportChanged) {
|
||||||
|
this.decorations = wrapLines(update.view, wrapElements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (v) => v.decorations,
|
||||||
}
|
}
|
||||||
}, {
|
);
|
||||||
decorations: v => v.decorations,
|
|
||||||
});
|
|
||||||
|
@ -1,60 +1,86 @@
|
|||||||
import { styleTags } from '@codemirror/highlight';
|
import { styleTags } from "@codemirror/highlight";
|
||||||
import { MarkdownConfig } from "@lezer/markdown";
|
import { MarkdownConfig } from "@lezer/markdown";
|
||||||
import { commonmark, mkLang } from "./markdown/markdown";
|
import { commonmark, mkLang } from "./markdown/markdown";
|
||||||
import * as ct from './customtags';
|
import * as ct from "./customtags";
|
||||||
|
|
||||||
const WikiLink: MarkdownConfig = {
|
const WikiLink: MarkdownConfig = {
|
||||||
defineNodes: ["WikiLink"],
|
defineNodes: ["WikiLink", "WikiLinkPage"],
|
||||||
parseInline: [{
|
parseInline: [
|
||||||
name: "WikiLink",
|
{
|
||||||
parse(cx, next, pos) {
|
name: "WikiLink",
|
||||||
let match: RegExpMatchArray | null;
|
parse(cx, next, pos) {
|
||||||
if (next != 91 /* '[' */ || !(match = /^\[[^\]]+\]\]/.exec(cx.slice(pos + 1, cx.end)))) {
|
let match: RegExpMatchArray | null;
|
||||||
return -1;
|
if (
|
||||||
}
|
next != 91 /* '[' */ ||
|
||||||
return cx.addElement(cx.elt("WikiLink", pos, pos + 1 + match[0].length));
|
!(match = /^\[[^\]]+\]\]/.exec(cx.slice(pos + 1, cx.end)))
|
||||||
},
|
) {
|
||||||
after: "Emphasis"
|
return -1;
|
||||||
}]
|
}
|
||||||
|
return cx.addElement(
|
||||||
|
cx.elt("WikiLink", pos, pos + match[0].length + 1, [
|
||||||
|
cx.elt("WikiLinkPage", pos + 2, pos + match[0].length - 1),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
},
|
||||||
|
after: "Emphasis",
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const AtMention: MarkdownConfig = {
|
const AtMention: MarkdownConfig = {
|
||||||
defineNodes: ["AtMention"],
|
defineNodes: ["AtMention"],
|
||||||
parseInline: [{
|
parseInline: [
|
||||||
name: "AtMention",
|
{
|
||||||
parse(cx, next, pos) {
|
name: "AtMention",
|
||||||
let match: RegExpMatchArray | null;
|
parse(cx, next, pos) {
|
||||||
if (next != 64 /* '@' */ || !(match = /^[A-Za-z\.]+/.exec(cx.slice(pos + 1, cx.end)))) {
|
let match: RegExpMatchArray | null;
|
||||||
return -1;
|
if (
|
||||||
}
|
next != 64 /* '@' */ ||
|
||||||
return cx.addElement(cx.elt("AtMention", pos, pos + 1 + match[0].length));
|
!(match = /^[A-Za-z\.]+/.exec(cx.slice(pos + 1, cx.end)))
|
||||||
},
|
) {
|
||||||
after: "Emphasis"
|
return -1;
|
||||||
}]
|
}
|
||||||
|
return cx.addElement(
|
||||||
|
cx.elt("AtMention", pos, pos + 1 + match[0].length)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
after: "Emphasis",
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
const TagLink: MarkdownConfig = {
|
const TagLink: MarkdownConfig = {
|
||||||
defineNodes: ["TagLink"],
|
defineNodes: ["TagLink"],
|
||||||
parseInline: [{
|
parseInline: [
|
||||||
name: "TagLink",
|
{
|
||||||
parse(cx, next, pos) {
|
name: "TagLink",
|
||||||
let match: RegExpMatchArray | null;
|
parse(cx, next, pos) {
|
||||||
if (next != 35 /* '#' */ || !(match = /^[A-Za-z\.]+/.exec(cx.slice(pos + 1, cx.end)))) {
|
let match: RegExpMatchArray | null;
|
||||||
return -1;
|
if (
|
||||||
}
|
next != 35 /* '#' */ ||
|
||||||
return cx.addElement(cx.elt("TagLink", pos, pos + 1 + match[0].length));
|
!(match = /^[A-Za-z\.]+/.exec(cx.slice(pos + 1, cx.end)))
|
||||||
},
|
) {
|
||||||
after: "Emphasis"
|
return -1;
|
||||||
}]
|
}
|
||||||
|
return cx.addElement(cx.elt("TagLink", pos, pos + 1 + match[0].length));
|
||||||
|
},
|
||||||
|
after: "Emphasis",
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
const WikiMarkdown = commonmark.configure([WikiLink, AtMention, TagLink, {
|
const WikiMarkdown = commonmark.configure([
|
||||||
|
WikiLink,
|
||||||
|
AtMention,
|
||||||
|
TagLink,
|
||||||
|
{
|
||||||
props: [
|
props: [
|
||||||
styleTags({
|
styleTags({
|
||||||
WikiLink: ct.WikiLinkTag,
|
WikiLink: ct.WikiLinkTag,
|
||||||
AtMention: ct.MentionTag,
|
WikiLinkPage: ct.WikiLinkPageTag,
|
||||||
TagLink: ct.TagTag,
|
AtMention: ct.MentionTag,
|
||||||
})
|
TagLink: ct.TagTag,
|
||||||
]
|
}),
|
||||||
}]);
|
],
|
||||||
/// Language support for [GFM](https://github.github.com/gfm/) plus
|
},
|
||||||
/// subscript, superscript, and emoji syntax.
|
]);
|
||||||
|
|
||||||
export default mkLang(WikiMarkdown);
|
export default mkLang(WikiMarkdown);
|
||||||
|
@ -1,20 +1,27 @@
|
|||||||
import { Action, AppViewState } from "./types";
|
import { Action, AppViewState } from "./types";
|
||||||
|
|
||||||
export default function reducer(state: AppViewState, action: Action): AppViewState {
|
export default function reducer(
|
||||||
console.log("Got action", action)
|
state: AppViewState,
|
||||||
|
action: Action
|
||||||
|
): AppViewState {
|
||||||
|
console.log("Got action", action);
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "loaded":
|
case "note-loaded":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
currentNote: action.name,
|
currentNote: action.name,
|
||||||
isSaved: true,
|
isSaved: true,
|
||||||
};
|
};
|
||||||
case "saved":
|
case "note-saved":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isSaved: true,
|
isSaved: true,
|
||||||
};
|
};
|
||||||
case "updated":
|
case "note-updated":
|
||||||
|
// Minor rerender optimization, this is triggered a lot
|
||||||
|
if (!state.isSaved) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isSaved: false,
|
isSaved: false,
|
||||||
@ -22,17 +29,28 @@ export default function reducer(state: AppViewState, action: Action): AppViewSta
|
|||||||
case "start-navigate":
|
case "start-navigate":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isFiltering: true,
|
showNoteNavigator: true,
|
||||||
};
|
};
|
||||||
case "stop-navigate":
|
case "stop-navigate":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isFiltering: false,
|
showNoteNavigator: false,
|
||||||
};
|
};
|
||||||
case "notes-list":
|
case "notes-listed":
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
allNotes: action.notes,
|
allNotes: action.notes,
|
||||||
};
|
};
|
||||||
|
case "show-palette":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showCommandPalette: true,
|
||||||
|
};
|
||||||
|
case "hide-palette":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
showCommandPalette: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
return state;
|
||||||
|
}
|
||||||
|
@ -1,32 +1,33 @@
|
|||||||
import { HighlightStyle, tags as t } from '@codemirror/highlight';
|
import { HighlightStyle, tags as t } from "@codemirror/highlight";
|
||||||
import * as ct from './customtags';
|
import * as ct from "./customtags";
|
||||||
|
|
||||||
export default HighlightStyle.define([
|
export default HighlightStyle.define([
|
||||||
{ tag: t.heading1, class: "h1" },
|
{ tag: t.heading1, class: "h1" },
|
||||||
{ tag: t.heading2, class: "h2" },
|
{ tag: t.heading2, class: "h2" },
|
||||||
{ tag: t.link, class: "link" },
|
{ tag: t.link, class: "link" },
|
||||||
{ tag: t.meta, class: "meta" },
|
{ tag: t.meta, class: "meta" },
|
||||||
{ tag: t.quote, class: "quote" },
|
{ tag: t.quote, class: "quote" },
|
||||||
{ tag: t.monospace, class: "code" },
|
{ tag: t.monospace, class: "code" },
|
||||||
{ tag: t.url, class: "url" },
|
{ tag: t.url, class: "url" },
|
||||||
{ tag: ct.WikiLinkTag, class: "wiki-link" },
|
{ tag: ct.WikiLinkTag, class: "wiki-link" },
|
||||||
{ tag: ct.TagTag, class: "tag" },
|
{ tag: ct.WikiLinkPageTag, class: "wiki-link-page" },
|
||||||
{ tag: ct.MentionTag, class: "mention" },
|
{ tag: ct.TagTag, class: "tag" },
|
||||||
{ tag: t.emphasis, class: "emphasis" },
|
{ tag: ct.MentionTag, class: "mention" },
|
||||||
{ tag: t.strong, class: "strong" },
|
{ tag: t.emphasis, class: "emphasis" },
|
||||||
{ tag: t.atom, class: "atom" },
|
{ tag: t.strong, class: "strong" },
|
||||||
{ tag: t.bool, class: "bool" },
|
{ tag: t.atom, class: "atom" },
|
||||||
{ tag: t.url, class: "url" },
|
{ tag: t.bool, class: "bool" },
|
||||||
{ tag: t.inserted, class: "inserted" },
|
{ tag: t.url, class: "url" },
|
||||||
{ tag: t.deleted, class: "deleted" },
|
{ tag: t.inserted, class: "inserted" },
|
||||||
{ tag: t.literal, class: "literal" },
|
{ tag: t.deleted, class: "deleted" },
|
||||||
{ tag: t.list, class: "list" },
|
{ tag: t.literal, class: "literal" },
|
||||||
{ tag: t.definition, class: "li" },
|
{ tag: t.list, class: "list" },
|
||||||
{ tag: t.string, class: "string" },
|
{ tag: t.definition, class: "li" },
|
||||||
{ tag: t.number, class: "number" },
|
{ tag: t.string, class: "string" },
|
||||||
{ tag: [t.regexp, t.escape, t.special(t.string)], class: "string2" },
|
{ tag: t.number, class: "number" },
|
||||||
{ tag: t.variableName, class: "variableName" },
|
{ tag: [t.regexp, t.escape, t.special(t.string)], class: "string2" },
|
||||||
{ tag: t.comment, class: "comment" },
|
{ tag: t.variableName, class: "variableName" },
|
||||||
{ tag: t.invalid, class: "invalid" },
|
{ tag: t.comment, class: "comment" },
|
||||||
{ tag: t.punctuation, class: "punctuation" }
|
{ tag: t.invalid, class: "invalid" },
|
||||||
|
{ tag: t.punctuation, class: "punctuation" },
|
||||||
]);
|
]);
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
:root {
|
||||||
|
--ident: 18px;
|
||||||
|
--editor-font: "Avenir";
|
||||||
|
--top-bar-bg: rgb(41, 41, 41);
|
||||||
|
/* --editor-font: "Menlo"; */
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -7,21 +14,26 @@ body {
|
|||||||
|
|
||||||
#top {
|
#top {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background-color: #eee;
|
background-color: var(--top-bar-bg);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
color: #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
#bottom {
|
#bottom {
|
||||||
height: 40px;
|
position: absolute;
|
||||||
background-color: #eee;
|
|
||||||
margin: 0;
|
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
position: absolute;
|
height: 25px;
|
||||||
|
background-color: var(--top-bar-bg);
|
||||||
|
color: #eee;
|
||||||
|
margin: 0;
|
||||||
|
padding: 5px;
|
||||||
|
font-family: var(--editor-font);
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
#editor {
|
#editor {
|
||||||
@ -33,10 +45,6 @@ body {
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
|
||||||
--ident: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -44,7 +52,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor .cm-content {
|
.cm-editor .cm-content {
|
||||||
font-family: "Menlo";
|
font-family: var(--editor-font);
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +100,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor .meta {
|
.cm-editor .meta {
|
||||||
color: #520130;
|
color: #650007;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor .line-blockquote {
|
.cm-editor .line-blockquote {
|
||||||
@ -119,9 +127,12 @@ body {
|
|||||||
color: #7e7d7d;
|
color: #7e7d7d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor .wiki-link {
|
.cm-editor .wiki-link-page {
|
||||||
color: #0330cb;
|
color: #0330cb;
|
||||||
/*text-decoration: underline;*/
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.cm-editor .wiki-link {
|
||||||
|
color: #808080;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-editor .mention {
|
.cm-editor .mention {
|
||||||
@ -137,43 +148,40 @@ body {
|
|||||||
margin-left: var(--ident);
|
margin-left: var(--ident);
|
||||||
}
|
}
|
||||||
|
|
||||||
reach-portal input {
|
|
||||||
background-color: #fff;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
reach-portal > div > div {
|
|
||||||
background-color: #fff;
|
|
||||||
border: #000 1px solid;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.current-note {
|
.current-note {
|
||||||
font-family: "Menlo";
|
font-family: var(--editor-font);
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container {
|
.filter-container {
|
||||||
font-family: "Menlo";
|
font-family: var(--editor-font);
|
||||||
background-color: white;
|
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
z-index: 1000;
|
|
||||||
border: #333 1px solid;
|
border: #333 1px solid;
|
||||||
|
z-index: 1000;
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: 8px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-container .result-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container input {
|
.filter-container input {
|
||||||
font-family: "Menlo";
|
font-family: var(--editor-font);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* border: 1px #333 solid; */
|
/* border: 1px #333 solid; */
|
||||||
|
background-color: var(--top-bar-bg);
|
||||||
|
color: #eee;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 1px #333 dotted;
|
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
|
outline: 0;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-container .option,
|
.filter-container .option,
|
||||||
|
@ -3,18 +3,27 @@ export type NoteMeta = {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AppCommand = {
|
||||||
|
name: string;
|
||||||
|
run: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
export type AppViewState = {
|
export type AppViewState = {
|
||||||
currentNote: string;
|
currentNote: string;
|
||||||
isSaved: boolean;
|
isSaved: boolean;
|
||||||
isFiltering: boolean;
|
showNoteNavigator: boolean;
|
||||||
|
showCommandPalette: boolean;
|
||||||
allNotes: NoteMeta[];
|
allNotes: NoteMeta[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Action =
|
export type Action =
|
||||||
| { type: "loaded"; name: string }
|
| { type: "note-loaded"; name: string }
|
||||||
| { type: "saved" }
|
| { type: "note-saved" }
|
||||||
|
| { type: "note-updated" }
|
||||||
|
| { type: "notes-listed"; notes: NoteMeta[] }
|
||||||
| { type: "start-navigate" }
|
| { type: "start-navigate" }
|
||||||
| { type: "stop-navigate" }
|
| { type: "stop-navigate" }
|
||||||
| { type: "updated" }
|
| { type: "show-palette" }
|
||||||
| { type: "notes-list"; notes: NoteMeta[] };
|
| { type: "hide-palette" }
|
||||||
|
;
|
||||||
|
|
||||||
|
9
webapp/src/util.ts
Normal file
9
webapp/src/util.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export function countWords(str: string): number {
|
||||||
|
var matches = str.match(/[\w\d\'\'-]+/gi);
|
||||||
|
return matches ? matches.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readingTime(wordCount: number): number {
|
||||||
|
// 225 is average word reading speed for adults
|
||||||
|
return Math.ceil(wordCount / 225);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user