Formal Works

Prototype 1: Agenda Embryo
Login

Conceptual Overview

Summary

Agenda Embryo is a simple, composable to-do list app.

It is, in essence, a tool for organizing tasks and recording the completion of those tasks. We will refer to tasks as entries.

Note on Simplicity: As this will be the first usable prototype, it would be imprudent to try to pin down architectural decisions. For this reason we will choose based on what is the simplest to implement and understand.

Agenda Embryo mockup for display on iPhone

Basic Elements

The app is composed of three basic elements:

  1. Entries
  2. Group entries
  3. Entry actions
  4. Views

You can organize entries by grouping them within a group entry. By default the app comes with several groups ready for you to use:

If the pre-supplied groups don't match your needs, you can easily rename, remove, or rearrange them.

You can manipulate your entries by performing actions on them. These actions are accessible via the entry toolbar that appears below an entry when you tap on it. Group entries have entry toolbars as well, the only difference being that the actions apply to the whole group.

Views allow you to view your entries organized in ways that might be helpful for different purposes. For Agenda Embryo, we will include only two views. The TO DO view shows all uncompleted entries, and the DONE view shows all entries that you've completed. This is discussed further in the Archive section.

Entry Actions

There are several entry actions that live on the entry toolbar:

The actions on a group entry toolbar are slightly different:

*Questions:*

Archive

When you complete a task, you can mark it as complete. When you view your completed tasks, you will see a record of the tasks you've completed, organized in the order in which you've completed them. This is the archive.

Each entry in the archive has an entry toolbar with two actions. One to move the entry back to the TO DO list, and another to delete the entry if you just don't want to see it any more.

Should the entries in the archive be listed in order of date completed by default? Should they be organized based on the groups that they were a part of when you completed them? Listing in the order completed is simpler, because we don't have to handle mismatches between the organization in TO DO and DONE. That's why we'll list them in order of date completed for now.

Rearranging

Drag-and-drop is a pain in the ass. It's hard to move to a position off-screen, hard to see under your finger, and inherently slow. Let's try something a little different.

To begin moving an entry, you bring up its toolbar and tap the MOVE action. The entry then gets pinned to the top of the screen, and you can move over your list and tap wherever you want to move it to. It then drops in right where you tapped.

Maybe the entries spread out a little, or the space between them glows or changes color, so you know that you can drop the entry in there.

Navigation

Most people don't want to see all of their tasks all of the time.

Most often, you probably want to see the things you have marked for your "Right Now" list. Or maybe you'd like an overview and would like to see your group entries.

By default all groups are contracted. All you see are group entries.

In the next version, perhaps the "Right Now" list will be expanded by default. That is most likely what is most relevant to a person when they first come to the app. We are opting to keep it contracted for now because that makes it simpler, since there need be nothing special about the "Right Now" list if we contract it like all the other group entries.

The view where you see everything in your master list is probably a bit overwhelming for most situations. It is probably useful for managing and reorganizing your tasks, but that is probably not something people will want to do as often as they want to see a limited scope.

Moving into and out of groups through tapping on the group entries is the core of navigation in Agenda Embryo.

You can tap on a group entry to focus on that group and display the group entry and all contained entries. Everything else will be hidden. You can scroll through the entries if there are enough to scroll.

When you enter into a group, above the group entry and its children entries sits the parent group's entry. You can tap on the parent group entry to return to the parent group.

The new entry input stays visible at the top of the current group you're in. This enables you to add things super easily but it won't crowd the UI because there's only one of them.

The group toolbar of the group in focus is also always displayed just below the group entry.

Usability Questions

Group Entry Toolbars

All group entry toolbars live fixed just below their group entry when that group is the current one.

When you hit MOVE on a group entry toolbar, the group entry is fixed to the top of the screen, and you are returned to the parent group so that you may place it in a new location.

Master Toolbar

Conceptually, all groups are contained in one group that we'll call the master group.

At the top of the master group is an entry toolbar that we'll call the master toolbar. The master toolbar has the following actions:

You don't see or interact with the master group directly, but it is an important concept for understanding how the interaction with the main toolbar is unified with the entry toolbars.

If you add an entry from the master entry toolbar, it will sit at the top of the master group by default. It can be a defacto uncategorized group, a sort of inbox. Or however else you'd like to use entries that sit ungrouped on top of your groups.

When you scroll down, the master entry toolbar will scroll away, just like any other entry toolbar.

Guiding Principles

Usage and Purpose

Design

Who is it for?

Exemplar Use Cases

This is intended to be a concise list of a few exemplar use case narratives, followed by descriptions of a possible workflow through the app to fulfill the needs of that use.

The idea is that the app should be able to accomodate many different organizational needs and workflows. These workflows represent the kinds of users we are building the app for.

Busy Freelance New Yorker

Story:

As a freelancer I juggle a lot of projects, so staying organized with them is essential. I keep track of a lot of things, so it's really important that I can add to, or view my tasks quickly.

As I'm planning my week/day, I'd like to be able to see my appointments, scheduled tasks, and deadlines all in one place.

As I'm looking back over the week/day, I'd like to be able to see how I've spent my time on each project I'm working on.

I'd also like to be able to make quick lists (e.g. shopping list) that I can use without interfering with my work lists.

Workflow:

Manager of a design office

Eli: This are real requests from Jo (married to Max, Noah Emrich's brother). She said she has a list written down of all the things she wants from a TODO app, and she said she hasn't found anything that fit them all.

Story:

I manage a design office, so I'm responsible for creating and communicating all of the project timelines and priorities. I'm not great with technology, so the more visual the tool can be, the better I'll be able to use it.

One part of my job is making calendars for everybody in the office to use so we can all sync up. These calendars need to be able to display multi-day projects and how they overlap. It also needs to look good and be easy to read, otherwise nobody will use it. Anything that can help make this easier would be great.

I'm adding things to the agenda all the time, so this should be a super quick process. The less I have to do the, better. And I should be able to do it on my phone, or on my computer. Online and offline ideally. Evernote's Mac toolbar input widget is great, and I'd love something like that for this.

Workflow:

Clojurescript UI Management

We need a solution to manage front-end application state and UI rendering. The libraries we are considering are:

Om

Pros:

*Cons:*

Rum

Pros:

*Cons:*

Decision

We have decided to go with Rum, as it seems easier to use, and more extensible.

The ability to define new components, and mixins to change how those components handle state, will most likely be super useful.

Note from <2015-08-02 Sun>:

Through using Rum to build this prototype, a number of limitations have arisen. We will most likely use Om for the next prototype, as it has become clear that is far more mature, part of a growing ecosystem, and provides more advanced functionality right away. The next incarnation of Om seems like it will feature many improvements as well.

Discussion & Resources

The base of the app is a Clojurescript app that uses Rum. We will integrate this into iOS, but first let's just set up our app a little.

Note: For reference, you can check out Rum on Github. You can also check out tonsky's datascript ToDo sample app as a reference for building an app with Rum.

Rum is based on the idea that you keep your state all in one place (e.g. datascript or atom). You organize your UI in terms of components that react to changes to that state, and automatically rerender when a relevant change occurs. The flow is bidirectional, so when you interact with the components through the UI, the state changes immediately, which in turn triggers a rerender that reflects the new state.

Setting Up the Dev Environment

We will be using boot to manage dependencies and builds. You can read about boot on Github. The first thing we need for boot is a boot.properties file for managing boot's version, and boot's Clojure version.

  BOOT_CLOJURE_VERSION=1.7.0
  BOOT_VERSION=2.1.2

Now we need to construct our build.boot file. Let's begin by setting some global boot options. We include Clojure and Clojurescript as dependencies, and set the sources and resources paths.

  (set-env!
    :source-paths   #{"src"}
    :resource-paths #{"../www"}
    :dependencies
    '[[adzerk/boot-cljs          "0.0-3308-0"      :scope "test"]
      [adzerk/boot-cljs-repl     "0.1.10-SNAPSHOT" :scope "test"]
      [adzerk/boot-reload        "0.3.1"           :scope "test"]
      [pandeiro/boot-http        "0.6.3-SNAPSHOT"  :scope "test"]
      [org.clojure/clojure       "1.7.0"]
      [org.clojure/clojurescript "0.0-3308"]
      [rum "0.2.7"]
      [org.omcljs/ambly "0.6.0"]])

To build Clojurescript with boot, we need to require the cljs task. We will also refer the cljs-repl, reload, and serve tasks for a nice dev workflow.

  (require
    '[adzerk.boot-cljs      :refer [cljs]]
    '[adzerk.boot-cljs-repl :refer [cljs-repl start-repl]]
    '[adzerk.boot-reload    :refer [reload]]
    '[pandeiro.boot-http    :refer [serve]]
    '[ambly.core :as ambly])

Now let's make a couple tasks. We'll a dev task for compiling our Clojurescript for development on the iPhone or simulator, and rel for compiling to a single file using advanced mode for release.

dev will compile without any optimizations, and rel will compile with advanced optimizations.

  (deftask dev []
    (set-env! :source-paths #{"src"})
    (comp (serve :dir "target/")
          (watch)
          (reload)
          (cljs-repl)
          (cljs :source-map true :optimizations :none)))

  (deftask rel []
    (comp (cljs :optimizations :advanced
                :main 'agenda.core)))

  <<boot/ambly>>

We also need to provide an entry point for the cljs task's compilation phase. We do this by adding a .cljs.edn file with a map with 3 required keys, and placing it in the source directory.

  {:require  [agenda.core agenda.css]
   :init-fns []
   :compiler-options {}}

And of course the compiled javascript will need a place to run. Let's make an HTML file that includes this script so we can see it in action. We'll also add elements for where we'll mount our list, and the input for our list. That's all we'll need there for now.

  <!doctype html>
  <html>
    <head>
      <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
      <link href="styles.css" rel="stylesheet" type="text/css">
    </head>
    <body>
      <div id="group-view"></div>
      <div id="view-control"></div>
      <script type='text/javascript' src='main.js'></script>
    </body>
  </html>

Running the App

To run the app for development, just do $ boot dev in the project directory (same one as build.boot).

Now if you navigate to http://localhost:3000/index.html, you should be able to see "Hello Rum!" in the console. Yay!!

To attach a REPL to the browser, enter $ boot repl -c and then > (start-repl). Or if you're using CIDER, run cider-connect to the localhost port where nREPL is running, then enter > (start-repl).

Now that we're all set up, we'll start playing with Rum.

Running a Clojurescript REPL into an iOS App With Ambly

Setting Up Objective-C Side of REPL

Apple doesn't like cool languages like Clojure. We can still run Clojure within an iOS app using the JavaScriptCore runtime if we set up some glue code.

To get clojurescript code to be evaluated within the iOS runtime environment, we will be using the Ambly library. Ambly will enable us to set up a Clojurescript environment on top of the JavaScriptCore in iOS.

Let's write our App Delegate to get started. This is the file responsible for managing the app lifecycle. The header is usually automatically generated by Xcode, and the main thing to notice is that is has one property, and that property is a window. This is the window that is used to display anything on the screen for the whole app.

  #import <UIKit/UIKit.h>

  @interface AppDelegate : UIResponder <UIApplicationDelegate>

  @property (strong, nonatomic) UIWindow *window;

  @end

Now we'll write the implementation for the AppDelegate. First let's import JavaScriptCore, the Ambly server and context manager (ABYServer, ABYContextManager), and an AGDWebViewController class that we will write later.

  #import "AppDelegate.h"

  #import <JavaScriptCore/JavaScriptCore.h>
  #import "ABYContextManager.h"
  #import "ABYServer.h"
  #import "AGDWebViewController.h"

Our app delegate will have two internal properties, one each for the server and context manager.

  @interface AppDelegate ()

  @property (strong, nonatomic) ABYContextManager* contextManager;
  @property (strong, nonatomic) ABYServer* replServer;

  @end

We'll also want to log uncaught exceptions. Let's make a method for that.

  void uncaughtExceptionHandler(NSException *exception) {
      NSLog(@"CRASH: %@", exception);
      NSLog(@"Stack Trace: %@", [exception callStackSymbols]);
  }

Now let's write the implementation of the app delegate. This setup is for development only. First, we set the uncaught exception handler to our own method. Then we'll launch a web view, and hand the webview's JavsScript context over to Ambly so it can set it up.

  @implementation AppDelegate

  - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

      NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);

      <<wkwebview-hack>>
      <<webview-launch>>
      <<ambly-jscontext-dev>>

      return YES;
  }

Now let's set up our web view's JavaScript context for Ambly in development when we're running in DEBUG mode. We'll need a directory to keep the compiled Clojurescript files in, so we'll create a private document directory. Finally, we'll start the configured REPL.

  #ifdef DEBUG
  // Set up the compiler output directory
  NSURL* compilerOutputDirectory = [[self privateDocumentsDirectory] URLByAppendingPathComponent:@"cljs-out"];
  [self createDirectoriesUpTo:compilerOutputDirectory];

  // Set up our context
  JSGlobalContextRef contextRef = [webViewController.webViewContext JSGlobalContextRef];
  self.contextManager = [[ABYContextManager alloc] initWithContext:contextRef
                                           compilerOutputDirectory:compilerOutputDirectory];
  [self.contextManager setupGlobalContext];
  [self.contextManager setUpConsoleLog];
  [self.contextManager setUpTimerFunctionality];
  [self.contextManager setUpAmblyImportScript];

  self.replServer = [[ABYServer alloc] initWithContext:contextRef
                               compilerOutputDirectory:compilerOutputDirectory];
  BOOL successful = [self.replServer startListening];
  if (!successful) {
      NSLog(@"Failed to start REPL server.");
  }
  #endif

We haven't defined our privateDocumentsDirectory method, so let's do that now.

  - (NSURL *)privateDocumentsDirectory
  {
      NSURL *libraryDirectory = [[[NSFileManager defaultManager] URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] lastObject];

      return [libraryDirectory URLByAppendingPathComponent:@"Private Documents"];
  }

We'll also need to define our createDirectoriesUpTo method.

  - (void)createDirectoriesUpTo:(NSURL*)directory
  {
      if (![[NSFileManager defaultManager] fileExistsAtPath:[directory path]]) {
          NSError *error = nil;

          if (![[NSFileManager defaultManager] createDirectoryAtPath:[directory path]
                                         withIntermediateDirectories:YES
                                                          attributes:nil
                                                               error:&error]) {
              NSLog(@"Can't create directory %@ [%@]", [directory path], error);
              abort();
          }
      }
  }

Awesome, that's it for the app delegate! Let's end it.

@end

And lest we not forget to include our dearly beloved Ambly library, let's write a podfile to pull it in.

  platform :ios, '8.0'
  pod "Ambly", "~> 0.6.0"

Running the Clojurescript REPL

Let's write a boot task to get our REPL up and running. We'll need to require cljs.repl and ambly.core, and launched into the Ambly REPL.

  (deftask ambly []
    (task-options!
      repl {:eval '(do
                    (require '[cljs.repl :as repl]
                             '[ambly.core :as ambly])
                    (repl/repl (ambly/repl-env)))
           })
    (repl))

Once the iOS app is running on a device or the simulator, you can connect by just running the boot command from the terminal using boot ambly, or from the boot REPL by calling (boot (ambly)).

For production, we'll want to compile the iOS app and copy the output directory to the resource bundle. We'll also want to add some code to the Objective-C side to set up the REPL nicely, and also set up everything to work in release builds.

iOS app, meet Clojurescript!

Let's set our iOS app up so that it can run our Clojurescript app. First we'll want to set up a webview on app launch, and load index.html into it. We're basically running a single-page web app inside our app. Let's take that HTML we wrote earlier and include it in our app bundle in a www directory.

Now let's get our webview. We'll write a class AGDWebViewController that we'll initialize with our HTML file. Let's pretend we've written it for a moment, and add the code we need in the app delegate to to set it up. We'll also need to set up our app's window first. We'll set it up to fill the entirety of the screen (always), and then we'll initialize our AGDWebViewController and give it the reins.

We'll initialize it our AGDWebViewController with our HTML using the custom initializer that we have yet to write.

  CGRect screenBounds = [[UIScreen mainScreen] bounds];

  UIWindow *window = [[UIWindow alloc] initWithFrame:screenBounds];

  AGDWebViewController *webViewController = [[AGDWebViewController alloc] initWithBaseURL:[NSURL fileURLWithPath:tempPath] loadURL:[NSURL URLWithString:@"index.html"]];

  [window setRootViewController:webViewController];

  [window makeKeyAndVisible];

  [self setWindow:window];

Now let's write our AGDWebViewController class. We'll have one property for the web view, one for it's JSContext, one for the URL we want to load initially, and another for the base URL if the web view needs to find other resources. We'll write a custom initializer, and this is the one that we'll use to set it up.

  #import <UIKit/UIKit.h>

  @class WKWebView;
  @class JSContext;

  @interface AGDWebViewController : UIViewController

  @property (nonatomic, strong) WKWebView *webView;
  @property (nonatomic, weak) JSContext *webViewContext;
  @property (nonatomic, strong) NSURL *loadURL;
  @property (nonatomic, strong) NSURL *baseURL;

  - (id)initWithBaseURL:(NSURL*)baseURL loadURL:(NSURL*)loadURL;

  @end

When our view loads, we'll want to set the frame to fill the screen, as this is where we'll do everything in our app. We'll also want to load the initial HTML string if it has already been provided. Finally, let's write our custom initializer to just set the view controller's properties.

There are a couple styling quirks that now would be a great time to deal with. The iOS status bar by default overlaps any content in the app (you can ask why, but you won't get far). We'd like to offset the web view by the height of the status bar (20px) and color the background view with our taupe from the app.

We do this by adding our webview to a subview, coloring the parent view taupe, and offsetting the web view while adjusting its height accordingly.

We also hide the scroll bars in the web view.

  #import "AGDWebViewController.h"
  #import <JavaScriptCore/JavaScriptCore.h>
  #import <WebKit/WebKit.h>

  @interface AGDWebViewController ()

  @end

  @implementation AGDWebViewController

  - (void)loadView {
      [super loadView];
      CGRect screen = [[UIScreen mainScreen] bounds];
      self.view = [[UIView alloc] initWithFrame:screen];
      self.view.backgroundColor = [UIColor colorWithRed:243.0/255 green:238.0/255 blue:224.0/255 alpha:1];

      CGRect frame = CGRectMake(CGRectGetMinX(screen),
                                CGRectGetMinY(screen) + 20,
                                CGRectGetWidth(screen),
                                CGRectGetHeight(screen) - 20);
      self.webView = [[WKWebView alloc] initWithFrame:frame];
      self.webView.scrollView.showsHorizontalScrollIndicator = NO;
      self.webView.scrollView.showsVerticalScrollIndicator = NO;        
      [self.view addSubview:self.webView];
  }

  - (void)viewDidLoad {
      [super viewDidLoad];
      if (self.loadURL) {
          NSURL *fullURL = [self.baseURL URLByAppendingPathComponent:[self.loadURL path]];
          NSLog(@"Initial URL to load: %@", [fullURL path]);
          [self.webView loadRequest:[[NSURLRequest alloc] initWithURL:fullURL]];
      }
  }

  - (id)initWithBaseURL:(NSURL*)baseURL loadURL:(NSURL*)loadURL {
      self = [super init];
      if (!self) {
          //init failed; abort
          return nil;
      }

      self.loadURL = loadURL;
      self.baseURL = baseURL;

      return self;
  }

  @end

We'll be using WKWebView, as it is newer and faster than UIWebView. WKWebView has a major bug where it cannot load local files. This is apparently fixed in iOS 9, but we will need to work around it for iOS 8 and earlier. As suggested in this StackOverflow answer, we will copy our web resource directory to tmp/www and access it via WKWebView from there.

  NSString *baseHTMLPath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html" inDirectory:@"target"];
  NSURL *baseURL = [NSURL URLWithString:[baseHTMLPath stringByDeletingLastPathComponent]];

  NSFileManager *fileManager = [NSFileManager defaultManager];
  NSString *tempPath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"www"];
  NSError *error = nil;

  [[NSFileManager defaultManager] removeItemAtPath:tempPath error:nil];

  NSLog(@"www path: %@", tempPath);
  [fileManager copyItemAtPath:[baseURL path] toPath:tempPath error:&error];
  if (error) {
    NSLog(@"Couldn't copy to tmp/www: %@", error);
   }

There is a big elephant in the room. We've established a JavaScript context with Ambly, but we need to be able to manipulate the context of the WKWebView so that we can change the DOM and make things appear on the screen. Apple has seemed to foil our plans for doing things the way we want once again... But no worries, we will prevail! There are two common methods discussed in a popular StackOverflow thread. The first uses undocumented key paths, and the second method works by adding a category to NSObject to get a WebKit (Mac library) callback. Both are sketchy, but it's just a workaround until the bug is fixed.

We'll need to communicate changes to the DOM to and from the web view's context, so let's get this context. We'll want to start out the app by running the main JavaScript file to get the whole thing in motion too.

  self.webViewContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
  [self.webViewContext evaluateScript:[[NSBundle mainBundle] pathForResource:@"target/main" ofType:@"js"]];

The Clojurescript App: Agenda Embryo

Basic Model Structure

The color pallette will be taupe and white for inactive areas, and turquoise/emerald for active areas.

We chose taupe turquoise emerald and white for the the color scheme because they look refined clean and sophisticated, yet calming and clear-headed.

Magenta or mauve are possible complementary additions. Maybe for different parts of the app.

We will be roughly following the workflow set out in the React documentation to build a static visual prototype of our Agenda app.

Our UI is, in a very real sense, simply a representation of data. We call the data we start from our data-model. We will start by writing some dummy data as simply a map with nested entries.

  (defonce mock-data-model
    (atom
     {:to-do
      {:group-entry {:text "To Do"}
       :color :turquoise
       :sub-entries []
       :sub-groups
       [{:group-entry {:text "Right Now"}
         :color :magenta
         :sub-entries
         [{:text "Call dentist"}
          {:text "Buy some more sponges for the kitchen"}
          {:text "Do laundry"}
          :sub-groups []
          ]}
        {:group-entry {:text "Personal"}
         :color :umber
         :sub-entries
         [{:text "Fix bowl"}
          {:text "Take out recycling"}
          {:text "Unify apartment color palette"}]
         :sub-groups
         [{:group-entry {:text "Home Improvements"}
           :color :periwinkle
           :sub-entries
           [{:text "Install chandelier"}
            {:text "Buy new sheets"}
            {:text "Buy new mattress"}
            :sub-groups
            [{:group-entry {:text "Eradicate all insects from premises"}
              :color :turquoise
              :sub-entries
              [{:text "Kill creepies"}
               {:text "Kill crawlies"}
               {:text "Invite ladybugs"}
               :sub-groups []
               ]}]]}]}
        {:group-entry {:text "Work"}
         :color :magenta
         :sub-entries
         [{:text "Finish this prototype"}
          {:text "Give it to people and see what they think"}]
         :sub-groups []}
        {:group-entry {:text "Errands"}
         :color :umber
         :sub-entries
         [{:text "Get more clothes"}
          {:text "Pick up magic beans from old lady"}]
         :sub-groups []}]}
      :done
      {:group-entry {:text "Done"}
       :color :periwinkle
       :sub-entries
       [{:text "Wake up"}
        {:text "Write to do list"}
        {:text "Go outside and face the world"}]
       :sub-groups []
       }}))

Our data-model will be transformed into a UI through a function that we'll call app-model. The app-model will also take into account the current state of the UI, which we will store in an atom called app-state.

Specifically, app-model is a function of data-model and app-state that returns an renderable UI in the form of HTML. The app-model is composed of components that we will build shortly.

data-model and app-state are conceptually distinct. data-model contains anything that we would want to persist from one session to another, while app-state contains transient information about the current state of the UI. Therefore we'll put our entries and their organization in data-model, and we'll put the current selected view in app-state.

We will have information about the possible views, the current view, and the entry in focus, if there is one. :focus-entry should be nil or a cursor to the entry.

We also include the current group visible for navigation purposes. :current-group should be a vector representing the path to the current group within the current view. If you are at the top-most level within the view, the path will be an empty vector.

  (def app-state
    (atom {:views [:to-do :done]
           :current-view :to-do
           :focus-entry nil
           :current-group []}))

We will use cursors to pass state down from app-state and data-model down to our base components. group-view will accept a cursor into data-model that will change according to what the current view is. It will update when any information within that view's subtree changes. view-control will simply accept the current view, and will update when the current view changes.

  (rum/defc app-model < rum/cursored rum/cursored-watch
    [data-model app-state]
    (group-view (rum/cursor data-model [(:current-view @app-state)]))
    (view-control (rum/cursor app-state [:views])))

Components Overview

Now let's write those components! So far we have to write group-view, which is the main section where we'll see our entries in a list, and view-control, which is the bar at the bottom that we can use to toggle the view between active and inactive entries.

Group View

Within group-view, we'll also want a component to represent an individual entry, which we'll call entry. Each agenda entry will be rendered with the entry component.

We'll want to keep track of the depth of the displayed entry, as that will affect how it gets displayed. So for each entry at a given level, we'll use the entry component to render the entries' text with the current depth specified as an argument. For each entry we will also check if there are sub-entries, in which case we will, for each sub-entry list, apply our group-view function again with an incremented depth and the sub-entries as arguments.

We will write group-view as a vanilla function that will return a vector of entry components that take cursors and watch them. Every time we enter into a new level of entries, we will increment the depth and append a new index to path starting from 0. in which the sub-entries reside. Each time we move to the next entry within the same level, we will increment the last path component (i.e. at index depth) of the path vector and pass the unchanged depth right through.

We append a number to the the group's class so that we can distinguish neighboring groups from each other visually.

  (rum/defc group-view < rum/cursored rum/cursored-watch
    [group-cursor]
    [:.group-view

     ;; our group entry for the current group
     (group-entry group-cursor)

     ;; the group entry toolbar for the current group
     (group-toolbar group)

     ;; add an entry to current group
     (entry-input "" group)

     ;; PREFACE
     ;; there should be a more elegant way to do the following
     ;; (using map probably) but it's tricky grabbing the indices
     ;; for the cursor path using .indexOf

     ;; sub-entries in the current group
     (for [index (range (count (:sub-entries @group)))
           :let [sub-entry
                 (rum/cursor group-cursor [:sub-entries index])]]
       (entry sub-entry))

     ;; group entries of sub-groups in the current group
     (for [index (range (count (:sub-groups @group)))
           :let [sub-group
                 (rum/cursor group-cursor [:sub-groups index])]]
       (group-entry sub-group))])

Entry

We'll write entry as a component that takes a cursor into data-model where it can access the text it needs to display. It includes an entry-toolbar that will hide and show itself based on whether or not that entry is in focus.

We'll also attach a handler to the click event so that we can take the entry in and out of focus.

  (rum/defc entry < rum/cursored rum/cursored-watch
    [entry-cursor]
    [:.entry-container
     {:on-click #(focus-entry entry-cursor)}
     [:.entry
      (:text @entry-cursor)]
     (entry-toolbar entry-cursor)])

Let's write our focus-entry function, which will put the entry in focus, bringing its toolbar out.

  (defn focus-entry [entry-cursor]
    (swap! app-state assoc :focus-entry
           (if (= entry-cursor (:focus-entry @app-state))
             nil
             entry-cursor)))

Group Entry

group-entry will take a cursor into data-model to its relevant group-entry.

  (rum/defc group-entry < rum/cursored rum/cursored-watch
    [group-cursor]
    [:.group-entry-container
     {:on-click #(enter-group group-cursor)}
     [(keyword (str ".group-entry.group-entry-" (:color @group-cursor)))
      (->> @group-cursor
           :group-entry
           :text)]])

We'll include an enter-group function for navigation. enter-group will navigate to the group that it is given a cursor to. This is the only navigation function we'll need, as we can use it to navigate both up and down.

  (defn enter-group [group-entry-cursor]
    (swap! app-state assoc :current-group group-entry-cursor))

View Control

view-control will be composed of multiple view-button instances. view-control will take a collection of view keywords, one for each view to be represented on the control. In this first iteration we only have :to-do and :done. view-button will be a simple <div> that displays the stringified keyword.

  (rum/defc view-button < rum/static [keyword]
    [:.view-button (-> (name keyword)
                         .toUpperCase
                         (clojure.string/replace "-" " "))])

  (rum/defc view-control < rum/cursored [views]
    [:.view-control
     (map view-button @views)])

Let's write a little convenience function for getting a page element based on its id. We'll use this in just a moment.

  (defn element [id] (.getElementById js/document id))

Mounting Our Components

We then need to mount everything onto the main document body. Our HTML file consists of only one div with an id of root, and this is where we'll mount the app-model.

  (rum/mount
   (group-view (rum/cursor mock-data-model [(:current-view @app-state)]))
   (element "group-view"))

  (rum/mount
   (view-control (rum/cursor mock-data-model [(:views @app-state)]))
   (element "view-control"))

Putting It Together

Now we need to put everything we've written into a namespace. Let's require Rum, and log a greeting to the console.

  (ns agenda.core
    (:require rum))

  (enable-console-print!)
  (println "We're live!!!")

  <<agenda.core/mock-data-model>>
  <<agenda.core/app-state>>
  <<agenda.core/element>>
  <<agenda.core/entry-toolbar>>
  <<agenda.core/group-toolbar>>
  <<agenda.core/entry-input>>
  <<agenda.core/focus-entry>>
  <<agenda.core/entry>>
  <<agenda.core/enter-group>>
  <<agenda.core/group-entry>>
  <<agenda.core/group-view>>
  <<agenda.core/view-control>>
  <<agenda.core/app-model>>
  <<agenda.core/mount>>

Events and Interactions

So we have a sexy beautiful interface, but it doesn't do anything yet. Let's get on that! We have these actions to implement:

As the app grows we will likely need to find a better way to manage updating app state from a component, but for now we wil simply operate on the state passed into the components. For the scale of this prototype, it shouldn't incur too much complexity.

Some other approaches involve using core.async and look promising, and a new version of Om will be released soon that addresses this issue.

Group Entry Toolbar

  (rum/defc group-toolbar < rum/reactive [entry]
    [:.group-toolbar])

Entry Toolbar

Most actions that can be performed on entries are accessed via the entry toolbar. The toolbar appears and disappears when you tap on the entry. There are buttons on the toolbar which you can tap on to perform those actions on the selected entry.

  (rum/defc entry-toolbar < rum/reactive [entry]
    [:.entry-toolbar
     {:style {:display (if (= (:focus-entry (rum/react app-state)) entry)
                         "block"
                         "none")}}])

Entry Input

We'll start by adding the ability to add an entry to the master list. We'll need to define a new component first, containing a text area and an ADD button.

This component has local state for the text contained in the text area. Whenever the text changes, the state is updated. If the user hits the ADD button after entering text in the box, an entry will be added with any extraneous whitespace before or after the text removed, and the text input area will be cleared. If the entered text consists only of whitespace, nothing will be added.

  (rum/defcs entry-input < (rum/local "") [state text entries]
    (let [local (:rum/local state)]
      [:.entry-input
       [:textarea.entry-input-text
        {:value @local
         :on-change #(reset! local (.-target.value %))}]
       [:button.entry-input-button
        {:on-click
         (fn []
           (let [cleaned (clojure.string/trim @local)]
             (if (not= cleaned "")
               (do
                 (swap! entries #(into [{:text cleaned}] %))
                 (reset! local "")))))}
        "ADD"
        ]]))

Storage

All app data will be stored in one atom. We will simply write this atom to disk as an edn file, and read it into the atom on app launch. There will be no undo, and we will overwrite the old app data whenever we write it to disk.

CSS Strategy Discussion and Rationale

Goals

Keep the CSS styling clean, clear, easy, and maintainable.

The goal is to have clearly defined CSS classes, each falling into one of the following categories:

You will then be able to mix and match as you wish. Instead of changing the CSS to make layout changes, you should be able to simply change the class of the elements.

Options

  1. Plain CSS
  2. Use Garden to write stylesheets in Clojure
  3. Use plain Clojure maps to represent CSS styles, organize and convert to CSS using Clojure functions

Plain CSS Classes

Pros:

*Cons:*

*Preprocessor Discussion*

Some of the disadvantages of using plain CSS can be mitigated by using a preprocessor. For example, autoprefixer can be used to automatigically add browser prefixes for browsers that have a significant user share. Since proper layout on supported devices is crucial to our app, it would be better to have more granular control over how we use vendor prefixes ourselves. We also want an understanding of what vendor prefixes are necessary, and if we rely on a magical third party tool for that, we will not understand that part of our system.

Garden

Pros:

*Cons:*

Plain Clojure Maps

Pros:

*Cons:*

Spec for Clojure to CSS Converter

Basic Styles

We will semantically organize our CSS maps.

We will define a map for each CSS selector that contains a map of its properties. These properties will be mapped to the CSS selector.

We will apply styles primarily to only single classes. One element may have multiple classes, which means they would get the styles from each class.

We will separate layout from style in our CSS maps. Layout defines where and what size the rectangles are, while the style determines what they look like. These are semantically very different, and we may benefit from keeping them separate.

  (def layout
    {:.entry {:display "flex"
              :align-items "center"}
     :.group {:display "flex"
              :flex "1"}})

  (def style
    {:.entry  {:font-size "20px"
               :color "#DDD"}
     :.group  {:font-size "20px"
               :color "#DDD"}})

We can combine our maps together to combine all the information that we'll need to put in our stylesheet.

We merge the style and layout maps, and if any selectors are included in both, we merge the style and layout rules for that selector.

  (def rules
    (merge-with merge style layout))

You can write functions that take arguments and are capable of returning multiple CSS classes. If we want the font-size to change based on a number appended to the entry class name, for example, we can write our function like this:

  (defn entry [depth]
    {(keyword (str ".entry-" depth))
     {:display "flex"
      :align-items "center"}})

In which case we would write our style differently, utilizing a function call instead of a var reference. We could include multiple versions of the same class by mapping our class style function over collections of values, and then merging the resulting maps and collections of maps.

  (def rules (apply merge (map entry (range 0 5))))

You can do the same thing in TCL.

  proc entry {number} {
      dict create entry-$number {
          display flex align-items center
      }
  }

  set rules [eval dict merge [lmap i {0 1 2 3 4 5} {entry $i}]]

You can also use a collection of CSS selectors as a key in the stylesheet map for when you want the styles to be applied to each CSS selector.

  (def reset
    {#{:html :body :div}
     {:margin 0 :padding 0}})

Media Queries and Fonts Oh My!

You can also write media queries as plain clojure maps.

  {{:min-width "300px"}
   {:body
    {:background-color "red"}}}

The above clojure data structure could be used to output the following CSS.

  @media (min-width: 300px) {
      body {
          background-color: red;
      }
  }

We can also write @font-face rules using clojure data structures. In this case, a map of the properties for the font that we are creating acts as the key in the map. That is because those properties are what will uniquely identify the created font in the stylesheet map.

  (def roboto
    {{:font-family "\"roboto\""
      :font-weight "200"
      :font-style "normal"}
     {:src [{:url "fonts/roboto-thin.eot"}
            {:url "fonts/roboto-thin.eot?#iefix" :format "embedded-opentype"}
            {:url "fonts/roboto-thin.woff2" :format "woff2"}
            {:url "fonts/roboto-thin.woff" :format "woff"}
            {:url "fonts/roboto-thin.ttf" :format "truetype"}
            {:url "fonts/roboto-thin.svg#robotothin" :format "svg"}]}})

The above code would result in producing the following CSS.

  @font-face {
      font-family: 'roboto';
      font-weight: 200;
      font-style: normal;
      src: url('fonts/roboto-thin.eot'),
      url('fonts/roboto-thin.eot?#iefix') format('embedded-opentype'),
      url('fonts/roboto-thin.woff2') format('woff2'),
      url('fonts/roboto-thin.woff') format('woff'),
      url('fonts/roboto-thin.ttf') format('truetype'),
      url('fonts/roboto-thin.svg#robotothin') format('svg');
  }

We'd also like our CSS converter to be able to handle media queries. Despite the multifarious and inconsistent ways CSS lets you represent things, we'll stick to good old Clojure data, using the one that makes sense for the type of data we are representing.

We'll represent a media query as a map. They key of our @media map is the target media feature, itself represented as a map. Our value is the styles we wish to conditionally apply.

Again, we hasten to remind the reader that our implementation is lacking support for some important parts of the CSS spec, but it's good enough for our uses. If we need more later, we can add more support then. Why do that work now when we can do it later, as the old adage goes.

  (def desktop
    {{:min-width "415px"}
     {:body {:width "415px"}}})

The above code will be converted to the following CSS.

  @media (min-width: 415px) {
    body {
      width: 415px;
    }
  }

You can merge as many maps as you want together to create an entire superpowered stylesheet.

  (def rules (apply merge
               roboto
               desktop
               (merge-with merge layout style)
               (map entry (range 5))))

Writing to Our CSS File

Once we have our CSS string, we can easily write it to a file in our web resources directory.

  (spit "../www/styles-test.css" (css rules))

Notes and Ideas Farther Afield

Further into the project, if you feel like rethinking the CSS situation, check out the ideas in Nick Gallagher's post about HTML semantics.

We may use some modification of the BEM (Block, Element, Modifier) class naming convention.

If you want to do it all in Clojure, you probably can solve a lot of those problems. Class names can be automatically generated, for example.

Glen Maddern has an interesting idea for creating pseudo-namespaces by using HTML attributes, which he calls Attribute Modules for CSS. Might be useful?

Maybe it will be useful to generate our class names based on namespaces? Many of the annoying parts of CSS are due to a lack of namespaces. CSS Modules is one attempt to fix this with JS. Clojure namespaces are awesome.

Generating CSS from Clojure Data

CSS Compiler Namespace

We would like our CSS to be nicely organized and modular. Clojure gives us a great way to organize hierarchical relationships in code via namespaces. We will simply use the great facilities provided by clojure for this purpose.

We include all of our CSS utilities and styling information (represented as Clojure data) in a single namespace. We include the functions in an order where any function that relies on another function is defined after its dependent function. Clojure requires this.

  (ns agenda.css.compiler)

  <<css/prefixees>>
  <<css/vendor-prefix>>
  <<css/rule-string>>
  <<css/url-string>>
  <<css/src-string>>
  <<css/css-rule>>
  <<css/css>>
  <<css/css-rule:selector>>
  <<css/css-rule:selector-set>>
  <<css/css-rule:font-face>>
  <<css/css-rule:media>>

Gameplan and Multimethod Dispatch

We need a way to generate CSS from our clojure data. We can simply write a function (we'll call it css) to generate a string containing the CSS rules, and write another function to write this to a file.

We will include a parameter to toggle pretty-printing.

Our clojure CSS rules are passed in as a map, and depending on what that map includes, we'll want to process it differently. In clojure we have nice consistent syntax, but unfortunately the consistency fairies never had time to visit CSS. In order to generate @font-face and @media rules, we'll have to interpret the contents of the map a little differently.

As a consequence of our varied needs, we'll write a helper multimethod to convert each key-value pair in the rule map to a CSS string. Let's call that css-rule. It'll dispatch off of the key of the key-value pair.

Our multimethod's dispatch method will return a keyword that indicates what type of CSS rule the key-value pair represents.

These are the cases we'll cover, and the keyword that represents each:

  1. :font-face: If a top-level key is a map containing a :font-family key, it will be interpreted as a @font-face rule.
  2. :media If a top-level key is a map containing a :min-width or :max-width key, it will be interpreted as a @media rule
  3. :selector-set If the key is a set, it apply the value's rules to each selector in the set, ending up in the CSS as a series of comma-separated selectors.
  4. :selector If a top-level key is a keyword, it will be interpreted as a single CSS selector, with the value assumed to be a map of the layout and style rules to apply.

/Note that this differs starkly from the spec, especially concerning the @media rule. These are the only features we need for now, so they're the only features we'll support at the moment./

  (defn css [styles]
    (apply str (map #(apply css-rule %) styles)))


  ;; css-rule takes a key-value pair, returns a CSS string
  (defmulti css-rule
    (fn [key value]
      (cond
        (map? key) (cond
                     (contains? key :font-family)
                     :font-face
                     (or (contains? key :min-width)
                         (contains? key :max-width))
                     :media)
        (set? key) :selector-set
        (or (keyword? key)
            (string? key)) :selector)))

Sexy Single Selectors

For :selector rules containing styles to be applied to a single selector, it is a simple and straightforward conversion to build a corresponding CSS string. We first merge the layout and style rules. We then add to our empty string the selector and an opening bracket.

We then add each property-value pair, separated by a colon and punctuated by a semicolon. This is handled by rule-string, which we'll write to handle a few cases.

For our present situation, we'll use rule-string to add a new line with the property and it's value indented by 2 spaces, separated by a colon, and followed by a closing semicolon. This will be handled by an arity that 2 arguments: a property and its value. We also add an arity for taking 1 argument, a single map of rules, which will return the concatenation of applying rule-string to each property-value pair. This is the arity we'll use for our :selector rules.

We add an additional arity of 3 arguments to rule-string so we can pass it an options map to specify a :terse format, which we'll use later for media queries.

We finish our selector rule by adding a closing bracket and two newlines.

  (defmethod css-rule :selector [selector rules]
    (str (name selector) " {\n"
         (rule-string rules)
         "}\n\n"))


  (defn rule-string
    ([property value options]
     (case (:format options)
       :pretty (str "  " (name property) ": " value ";\n")
       :terse (str (name property) ": " value)))
    ([property value]
     (rule-string property value {:format :pretty}))
    ([rules]
     (apply str (map
                 #(apply rule-string %)
                 (vendor-prefix rules)))))

Selector Set Swinging

  (defmethod css-rule :selector-set [selector rules]
    (str (clojure.string/join ", " (map name selector)) " {\n"
         (rule-string rules)
         "}\n\n"))

This would be able to take something like this:

  (def list-style
    {#{:ol :ul}
     {:list-style "none"}})


  ol, ul {
    list-style: none;
  }

Font Face Freedom

We convert :font-face rules by building a string beginning with our @fontface declaration and an opening bracket. To this we add a string composed of the result of applying our previously defined rule-string to each key-value pair in the selector.

We then add @src: followed by the specified urls and formats, printed in the form of a list of functions or function pairs.

  (defmethod css-rule :font-face [selector rules]
    (str "@font-face {\n"
         (apply str (map #(apply rule-string %) selector))
         (src-string (:src rules))
         "}\n\n"))


  (defn src-string [source]
    (str "  src: "
         (clojure.string/join ", "
           (map
            #(url-string (:url %) (:format %))
            (filter #(contains? % :url) source)))
         ";\n"))


  (defn url-string
    ([url]
     (str "url('" url "')"))
    ([url format]
     (if (nil? format)
       (url-string url)
       (str (url-string url)
            " format('" format "')"))))

Media Query Mayhem

For now, we'll only support media queries applying to one specified media feature.

For the :media branch of our multimethod, we'll start with an @media string and an open parenthesis. To that we'll append the application of the terse version rule-string to our selector's rule. We then close the parenthesis, open a curly brace and insert a newline.

To this we simply add the result of using our css function on the rules, as those will just contain CSS styles. To make the formatting nice, we'll interleave each line with indentation and an newline.

We close by closing the media query's curly braces and adding two newlines.

  (defmethod css-rule :media [selector rules]
    (str "@media ("
         (apply rule-string (conj
                             (first selector)
                             {:format :terse}))
         ") {\n"
         (apply str (interleave
                     (repeat "  ")
                     (clojure.string/split-lines (css rules))
                     (repeat "\n")))
         "}\n\n"))

Cross-Browser Support Overview

Most modern browsers now happily support (relatively) recent web standards, but only if you happily add a special prefix to you attributes just for them.

Mark 12:13: "And when they were come, they say unto him, Teacher, we know that thou art true, and carest not for any one; for thou regardest not the person of men, but of a truth teachest the way of God: Is it lawful to give tribute unto Caesar, or not? 15Shall we give, or shall we not give? But he, knowing their hypocrisy, said unto them, Why make ye trial of me? bring me a denarius, that I may see it. 16And they brought it. And he saith unto them, Whose is this image and superscription? And they said unto him, Caesar's. 17And Jesus said unto them, Render unto Caesar the things that are Caesar's, and unto God the things that are God's. And they marvelled greatly at him."

Let's just give the browser lords their prefixes and move on.

To avoid having to remember and type all the prefixes ourselves, we will include an extra transformation step in our Clojure to CSS conversion process to automatically expand any rules to add the requisite vendor prefixes.

If you're in doubt of what browser support to add, you can use the Autoprefixer playground as a fun easy way to figure out what you should support.

We will only be releasing the app on Apple platforms first, so we'll only include -webkit- prefixes for now.

Generating Vendor Prefixed Rules

We will begin by adding support for flexbox. This CSS module is described in depth on the Mozilla Developer Network. We will support all properties described on that page.

  (def prefixees
    {:properties
     #{:align-content
       :align-items
       :align-self
       :flex
       :flex-basis
       :flex-direction
       :flex-flow
       :flex-grow
       :flex-shrink
       :flex-wrap
       :justify-content
       :order}
     :rule-values
     #{[:display "flex"]}})

Let's write a vendor-prefix function that will take a rule or collection of rules, and will return a new collection of rules that includes vendor prefixed rules.

For any property that should be prefixed, we add a new rule with a prefixed version of that property.

There are some cases where the value must be prefixed. For example, display: flex; becomes display: -webkit-flex;. We will add a new rule with prefixed properties for those as well.

We write one arity of vendor-prefix which will take a collection of rules. We apply our function to each rule, and ensure that the output collection of rules does not contain nested collections of rules, simply a one-level collection of rules.

  (defn vendor-prefix
    ([property value]
     (let [rule [property value]]
       (cond
         (contains? (:properties prefixees) property)
         [rule
          [(keyword (str "-webkit-" (name property))) value]]
         (contains? (:rule-values prefixees) rule)
         [rule
          [property (str "-webkit-" value)]]
         :default
         [rule])))
    ([rules]
     (->> (map #(apply vendor-prefix %) rules)
          flatten
          (partition 2)
          (map vec)
          vec
          )))

Note About Unsolved Mystery

In the following code, running (css entry) results in an infinite loop. I'm not sure why because I would instead think that after looping over all the elements in the styles sequence, it would not enter the recur branch, but would instead just return css-string and exit the loop, returning the current value of css-string.

If we place print statements inside, it appears like the collection gets emptied and then filled up again infinitely.

I'm not sure why, but I'd love to know the reason. It's probably just something silly.

  (defn css [styles]
    (loop [sytles styles
           css-string ""]
      (if (empty? styles)
        css-string
        (recur
         (rest styles)
         (str css-string
              (apply print (first styles))
              "\n")))))

Implementing Style

Managing with Namespaces

We have our style and layout in two different namespaces, so we don't have to worry about name collisions.

CSS is a funny beast. The order of your declarations changes the effects of the declarations. We will deal with this issue by implementing our rule maps as vectors of key-value pairs.

We had previously tried to use array-map, which keeps its keys in insertion order, but it's a little touchy. Some kinds of data transformations do not preserve the underlying type. There is no strong guarantee that it will be preserved internally as an PersistentArrayMap, and the key order may be compromised. We tried doing #(merge-with merge % %) on two array-maps, and the key order was compromised and the final result was a PersistentVector.

Style

  (ns agenda.css.style)

  <<css/style/base>>
  <<css/style/group-view>>
  <<css/style/entry-font>>
  <<css/style/entry>>
  <<css/style/group>>
  <<css/style/group-entry>>
  <<css/style/view-control>>
  <<css/style/view-button>>
  <<css/style/entry-toolbar>>
  <<css/style/entry-input>>
  <<css/style/entry-input-text>>
  <<css/style/entry-input-button>>

  (def style (concat base
                     group-view
                     entry
                     view-control
                     view-button
                     entry-toolbar
                     entry-input
                     entry-input-text
                     entry-input-button
                     (apply concat (map group (range 0 5)))
                     (apply concat (map group-entry (range 0 5)))
                     ))

Layout

  (ns agenda.css.layout)

  <<css/layout/base>>
  <<css/layout/entry-height>>
  <<css/layout/entry>>
  <<css/layout/view-control-height>>
  <<css/layout/view-control>>
  <<css/layout/view-button>>
  <<css/layout/group-view>>
  <<css/layout/entry-toolbar>>
  <<css/layout/entry-input>>
  <<css/layout/entry-input-text>>
  <<css/layout/entry-input-button>>

  (def layout (concat base
                      view-control
                      view-button
                      group-view
                      entry-toolbar
                      entry-input
                      entry-input-text
                      entry-input-button
                      (apply concat (map entry (range 0 5)))
                      ))

CSS Namespace

We put all of our CSS code in a css namespace. Our CSS reset, our fonts, and our merged style and layout rules for our CSS selectors.

  (ns agenda.css
    (:use [agenda.css.style :only [style]]
          [agenda.css.layout :only [layout]]
          [agenda.css.compiler :only [css]]))

  <<css/reset>>
  <<css/better-defaults>>
  <<css/roboto>>

  (def rules
    (css
     (concat reset
             better-defaults
             roboto
             style
             layout)))

Writing our CSS to Disk with Planck

We can make our lives a whole lot easier by automating the generation of our CSS file. Let's write a planck script that generates our CSS for us.

Planck is a utility tool for OS X that runs the bootstrapped Clojurescript compiler on JavaScriptCore. You can write fast-starting scripts using it, since it completely bypasses the JVM (which has a significant startup time).

All we need to do is write a -main function. The planck command line tool takes all the same arguments as the Clojure compiler.

  (ns agenda.planck
    (:require [agenda.css]
              [planck.core]))

  (defn -main []
    (do
      (planck.core/spit "../www/styles.css" agenda.css/rules)
      (planck.core/spit "../cljs/target/styles.css" agenda.css/rules)))

We can run this code with the following shell script. You should be in the prototype-1/cljs directory when you execute it.

  planck -c src -m agenda.planck

For development we will include a stylesheet directly in the compiler output directory.

CSS Reset

Browsers are, unfortunately, inconsistent amongst each other in how they render and style elements by default. Adding a CSS Reset at least addresses the CSS part of that issue, and lets us work on a clean slate for all browsers. They're all mostly the same, and we'll use a classic one courtesy of Eric Meyer, translated below into Clojure.

  (def reset
    [[#{:html :body :div :span :applet :object :iframe :h1 :h2 :h3 :h4 :h5 :h6 :p :blockquote :pre :a :abbr :acronym :address :big :cite :code :del :dfn :em :img :ins :kbd :q :s :samp :small :strike :strong :sub :sup :tt :var :b :u :i :center :dl :dt :dd :ol :ul :li :fieldset :form :label :legend :table :caption :tbody :tfoot :thead :tr :th :td :article :aside :canvas :details :embed :figure :figcaption :footer :header :hgroup :menu :nav :output :ruby :section :summary :time :mark :audio :video}
      {:margin "0"
       :padding "0"
       :border "0"
       :font-size "100%"
       :font "inherit"
       :vertical-align "baseline"}]
     [#{:figure :aside :figcaption :section :article :footer :header :details :hgroup :nav :menu}
      {:display "block"}]
     [:body {:line-height "1"}]
     [#{:ol :ul} {:list-style "none"}]
     [#{:blockquote :q} {:quotes "none"}]
     [#{:blockquote:before :blockquote:after :q:before :q:after}
      {:content "none"}]
     [:table {:border-collapse "collapse"
              :border-spacing "0"}]])

Note on Possible Alternatives:

Normalize.css is supposed to preserve useful browser defaults. We checked it out, but it didn't seem to help with any of our layout issues and it's bigger and not worth the time to work through the whole thing.

Working Against Strange CSS Defaults

By default, any text in a web view is selectable. This is not an interaction that we want to support, so we have to add some CSS to tell iDevices to disable this behavior.

  (def better-defaults
    [[:*
      {:-webkit-touch-callout "none"
       :-webkit-user-select "none"}]
     [:textarea
      {:-webkit-appearance "none"
       :border-radius "0px"}]])

Base Elements and Media Queries

Let's write a stylesheet that makes the list look the same on devices wider than iPhones (iPhone 6+ is 414px wide), and let's make the background a noticeable color.

  (def base
    [[:body {:background-color "#FFF"}]
     [:html {:background-color "#FFF"}]])


  (def base
    [[:body
       {:width "inherit"
        :margin "auto"
        :height "100%"
        :display "flex"
        :flex-direction "column"}]
     [:html
      {:height "100%"
       :width "100%"}]
     [{:min-width "320px"}
      {:body
       {:width "320px"}}]])

Fonts

We have chosen for now to use the free font Roboto for it's clean and consistent modern look. And because it's free. We include a few different font weights to use, adding each to the same font-family at a different weight.

  (def roboto
    [[{:font-family "\"roboto\""
        :font-weight "200"
       :font-style "normal"}
      {:src [{:url "fonts/roboto-thin.eot"}
             {:url "fonts/roboto-thin.eot?#iefix" :format "embedded-opentype"}
             {:url "fonts/roboto-thin.woff2" :format "woff2"}
             {:url "fonts/roboto-thin.woff" :format "woff"}
             {:url "fonts/roboto-thin.ttf" :format "truetype"}
             {:url "fonts/roboto-thin.svg#robotothin" :format "svg"}]}]
     [{:font-family "\"roboto\""
       :font-weight "300"
       :font-style "normal"}
      {:src [{:url "fonts/roboto-light.eot"}
             {:url "fonts/roboto-light.eot?#iefix" :format "embedded-opentype"}
             {:url "fonts/roboto-light.woff2" :format "woff2"}
             {:url "fonts/roboto-light.woff" :format "woff"}
             {:url "fonts/roboto-light.ttf" :format "truetype"}
             {:url "fonts/roboto-light.svg#robotolight" :format "svg"}]}]
     [{:font-family "\"roboto\""
       :font-weight "400"
       :font-style "normal"}
      {:src [{:url "fonts/roboto-regular.eot"}
             {:url "fonts/roboto-regular.eot?#iefix" :format "embedded-opentype"}
             {:url "fonts/roboto-regular.woff2" :format "woff2"}
             {:url "fonts/roboto-regular.woff" :format "woff"}
             {:url "fonts/roboto-regular.ttf" :format "truetype"}
             {:url "fonts/roboto-regular.svg#robotoregular" :format "svg"}]}]
     [{:font-family "\"roboto\""
       :font-weight "500"
       :font-style "normal"}
      {:src [{:url "fonts/roboto-bold.eot"}
             {:url "fonts/roboto-bold.eot?#iefix" :format "embedded-opentype"}
             {:url "fonts/roboto-bold.woff2" :format "woff2"}
             {:url "fonts/roboto-bold.woff" :format "woff"}
             {:url "fonts/roboto-bold.ttf" :format "truetype"}
             {:url "fonts/roboto-bold.svg#robotobold" :format "svg"}]}]])

Entries

We give the container a border on the top and bottom, and set that border inside the bounds of the element.

We'll also want to offset our container so that it doesn't get covered by the view buttons. Let's add a margin on the bottom of the whole thing equal to the height of the view-control.

  (def group-view
    [[:#group-view
      {:border-top "1px solid #FFF"
       :border-bottom "1px solid #FFF"
       :box-sizing "border-box"}]])

*Note:* We tried both padding-bottom and margin-bottom as well as targeting #group-view and .group-view, and some combinations worked on Safari, some worked on Chrome, but only the below worked on both. I'm not sure why, but this should work for now.

  (def group-view
    [["#group-view > .group-view"
      {:padding-bottom view-control-height}]])

Let's lay out our entries. We want our entries to take up the full width of their container, vertically stacked. This is the default behavior since divs by default have display set to block, so there's nothing to do for that!

We set a fixed height, give each element a fixed padding on the right, and set the left padding to vary based on the depth. This is to offset the shift to the entries caused by the color bars. If we didn't shift the entries left, the color bars would push the entries to the right.

To set the text to be vertically centered within the container, we set align-items to center. This only works if display is flex.

We write a function to generate a map of the properties for an entry at a specified depth.

  (def entry-height "40px")

clojure

  (defn entry [depth]
    [[(keyword (str ".entry-" depth))
      {:padding-left (str (- 20 (* 4 depth)) "px")
       :padding-right "20px"
       :padding-top "5px"
       :padding-bottom "5px"
       :align-items "center"
       :display "flex"}]])

For styling, let's set the font to a reasonable size of our font, and give a top and bottom border. We'll also set the background color to taupe.

  (def entry-font "18px \"roboto\"")

clojure

  (def entry
    [[#{:.entry
        :.group-entry}
      {:font entry-font
       :border-top "1px solid #FFF"
       :border-bottom "1px solid #FFF"
       :box-sizing "border-box"
       :background-color "#F3EEE0"
       :padding "5px 10px"}]])

Groups

Group entries are indented at the same level as their contained entries. This formatting is provided by the formatting for entries. Group entries are additionally bolded.

Groups are also visually distinguished from each other by a solid color bar that spans the whole group.

These colors are based off the colors used to distinguish neighboring territories from each other on old maps (a la ). Their purpose is similar, and they're pretty.

When a certain group is given a certain color, you tend to associate that group with that color. We should work with this tendency rather than against it, and let the colors persist. We assign a color to a group when it is created based on the colors surrounding it. It should be visually distinct, but complementary. The group's color will never change, except if the user reassigns it (not supported yet).

To implement this, we will need to include a group's color in our data-model.

We will give each group a line on the left. To allow these lines to show through the entries, we will give the groups a padding on the left, and give entries less padding according to how deeply they are nested.

border-left does not fulfill the intended purpose because it creates undesired diagonal corners where the borders meet. We can use box-shadow as a reasonable workaround.

  (def colors {:turquoise "#38C7AD"
               :magenta "#D789FF"
               :umber "#F5A05B"
               :periwinkle "#8FBEFF"})

  (defn group [depth]
    [[(keyword (str ".group-" depth))
      (let [i (mod depth 4)
            color (colors (case i
                            0 :turquoise
                            1 :magenta
                            2 :umber
                            3 :periwinkle))]
        {:padding-left "4px"
         :box-shadow (str "inset 4px 0 0 0 " color)})]])

We also want to make the relationship between a group entry and its contained entries, so let's give each group entry a hue of its group's color. We also want the font to be heavier.

  (def color-tints {:turquoise "#E0EADB"
                    :magenta "#F0E4E3"
                    :umber "#F3E6D3"
                    :periwinkle "#E9E9E3"})

  (defn group-entry [depth]
    [[(keyword (str ".group-entry-" depth))
      (let [i (mod depth 4)
            color (color-tints
                   (case i
                     0 :turquoise
                     1 :magenta
                     2 :umber
                     3 :periwinkle))]
        {:background-color color
         :font-weight 500})]])

View Control

We fix the view control to the bottom of the page by setting bottom to 0px and position to fixed. We set width to inherit to ensure that the contained view buttons fill the available width. We will also give it a fixed height. This is all on #view-control, as that is the outermost container.

On the inner container, .view-control, we set height to inherit so it takes up the available space in its container, and we'll give it a border. We also set display to flex so its contained elements can be evenly distributed using flexbox properties.

  (def view-control
    [[:.view-control
      {:border "1px solid #FFF"}]])


  (def view-control-height "40px")


  (def view-control
    [[:#view-control
      {:bottom "0px"
       :display "block"
       :position "fixed"
       :width "inherit"
       :height view-control-height}]
     [:.view-control
      {:display "flex"
       :height "inherit"}]])

For our view buttons we'll set our font to be a little heavier and turquoise. We horizontally align the text, and set the background to a slightly darker taupe. We'll also give the buttons a white border.

For this prototype we'll only have two buttons, so we'll just set the width to 50% and leave it at that.

  (def view-button
    [[:.view-button
      {:font entry-font
       :font-weight "500"
       :color "#00BAC1"
       :background-color "#EAE4D1"
       :border "1px solid #FFF"}]])

We want the text in our view buttons to be centered both vertically and horizontally. The way to do this without creating a wrapper div is to use a vertical flexbox layout, and to set :justify-content and :text-align to center. We'll also want the buttons to grow to fill the space and maintain even width, so we'll set flex-grow to 1.

  (def view-button
    [[:.view-button
      {:display "flex"
       :flex-direction "column"
       :justify-content "center"
       :text-align "center"
       :flex-grow "1"}]])

Entry Toolbar

We make the entry toolbar a fixed height and give it the same background color as a view button. We also give it a white border.

  (def entry-toolbar
    [[#{:.entry-toolbar
        :.group-toolbar}
      {:height entry-height}]])


  (def entry-toolbar
    [[#{:.entry-toolbar
        :.group-toolbar}
      {:background-color "#EAE4D1"
       :border "1px solid #FFF"}]])

Entry Inputs

We give our entry input elements the same font. We give the input area the same color as the entries, so it looks feels like an entry already. We give the text input a border to match the entry's border.

The entry input container div will need to have a padding on the left so that the entered text lines up with the entries of the same level.

We color the button the same as we do to the view buttons. A darker taupe and our active turquoise color.

  (def entry-input
    [[:.entry-input
      {:display "flex"
       :padding-left "15px"
       :height entry-height
       :align-items "center"}]])


  (def entry-input
    [[:.entry-input
      {:border-top "1px solid #FFF"
       :border-bottom "1px solid #FFF"
       :box-sizing "border-box"
       :background-color "#F3EEE0"}]])


  (def entry-input-text
    [[:.entry-input-text
      {:font entry-font
       :outline "none"
       :resize "none"
       :border "none"
       :background-color "#F3EEE0"
       :border-top "1px solid #FFF"
       :border-bottom "1px solid #FFF"
       :box-sizing "border-box"}]])


  (def entry-input-text
    [[:.entry-input-text
      {:height "inherit"
       :flex "1"}]])


  (def entry-input-button
    [[:.entry-input-button
      {:width "47px"
       :height "100%"}]])


  (def entry-input-button
    [[:.entry-input-button
      {:font "16px \"roboto\""
       :font-weight "500"
       :border "none"
       :background-color "#00BAC1"
       :color "#FFF"}]])

CSS Post-processing Discussion

We could (but do not) use autoprefixer with the PostCSS command-line tool to automatically generate the appropriate browser prefixes. This can be done on the command line by running postcss --use autoprefixer on the CSS files to convert. We could, for example, convert all CSS files in our web resources directory.

  postcss -u autoprefixer www/*.css -d www

This has the drawback of introducing a poorly understood component into our system. The rules under which autoprefixer operates are dependent on caniuse.com, which means that by definition it is unpredictable. We shouldn't build our system to rely on components that seem to "magically" make things work.

If instead we can just substitute the browser prefixes in as necessary, we will keep our program understandable. This will be easiest if our styles are represented as Clojure data, instead of CSS.

HTML Ideas

Rum comes with Sablono plugged in, so we will be using Sablono to generate markup. Sablono is just Hiccup for React.

You can check out Hiccup or Sablono libraries for resources.

Touch Handling

Touches and other user input can be represented as streams of events, which we can manage in Clojurescript using core.async. We can have channels for event streams we are interested in, write to those channels when the events fire, and read from those channels in any place that we are interested in that type of event.

Bruce Hauman has a great example of using core.async for managing touch handling in Clojurescript.

Testing and Program Validation

At some point we should really include some scaffolding to make sure our code works like we say it should.

Prismatic's Schema library seems very promising. You can define the shape of the data expected as input and output for functions, and optionally perform runtime validation for those functions.

This will be great for documentation purposes, and understanding each function in terms of input and output in a systematic formalized way.

You can now also use Schema to generate random data from the specified schemas for use with test.check generative testing. You can read about it on the Prismatic blog.

Prototype 2: Agenda Plenty

Clojurescript UI Library

Overview of Landscape

Om has broad support, Rum is interesting, Om Next seems super cool and simple (once it comes out) and Hoplon/Javelin seems conceptually simple.

Javelin doesn't have a virtual DOM, but the DOM updates are minimized by a real component dependency graph.

Using a virtual DOM, side effects are deduped. In Javelin, value updates are deduped. It's closer to the actual thing.

There are drawbacks to the virtual DOM approach that React takes.

The following snippet is adapted from an old React doc (alternate link):

  var MyComponent = React.createClass({
      handleClick: function() {
      // Explicitly focus the text input using the raw DOM API.
      React.findDOMNode(this.refs.myTextInput).focus();
    }
  })

You can see what a pain it is to use the raw DOM API from within React. React's virtual DOM abstraction takes you pretty far away from the real thing. This is quite a sacrifice to make, since you will likely want to use the native DOM API. It is the gateway to a whole realm of important functionality. Is a sacrifice like this, and the addition of all the complexity that comes with React, really a good compromise for the gained speed?

It might be sometime, but it definitely isn't worth it at the beginning.

If we should be postponing optimization until as late as we possibly can, we shouldn't use React just for efficiency reasons. If efficiency becomes a major concern later, we can look at options for optimization then. Using React might very well be one of those options.

One advantage of having access to the raw DOM API is that you have the ability to directly manipulate DOM events.

Why a Frontend Library at All?

React (and Clojurescript interfaces to it) and Hoplon (excluding Castra or Cljson) are essentially tools to make interactions with the stateful DOM less painful.

If you interact with the DOM using its stateful API directly, you will likely get into a state where your UI is inconsistent with your application state (read: bugs). This is very different than the functional programmer's ideal stateless API that would be designed with a uni-directional data flow from application state to interface elements.

Hoplon and React are attempts to build such a stateless interface on top of the existing stateful one.

We can actually break down this imaginary stateless interface to the stateful DOM into two components. The first is a DOM abstraction that coordinates changes to the DOM. The second is a data flow mechanism (a la reactive programming) that provides one-way flow from application state to the DOM abstraction.

Note on batching updates: React actually has a slightly broader scope, since it also provides a batching mechanism to batch updates to the DOM. This could probably be added to Hoplon as well. Javelin allows the use of functional lenses to provide convenient access to deep nested portions of larger data structures. You can use dosync to batch data changes to minimize DOM updates. This could be used to make batch DOM updates for each screen repaint using requestAnimationFrame.

Hoplon separates the data flow mechanism from the DOM abstraction, using Javelin and HLisp respectively. React conflates them in the concept of the virtual DOM.

Hoplon uses HLisp as the DOM abstraction. React uses the virtual DOM as the DOM abstraction.

Hoplon uses normal Clojurescript data structures and Javelin as a data flow mechanism. React uses the virtual DOM (JSX or React.DOM) for the data flow abstraction, with the application state residing in Javascript objects (or Clojurescript data structures if you're using Om etc.).

(HLisp in its current incarnation actually depends on Javelin, as it uses Javelin cells in its implementation. The two could (and probably should be) separated entirely, so a different mechanism for unidirectional data flow could be used if desired.)

Om and other Clojurescript interfaces to React usually try to apply Clojurescript's immutable data structure semantics onto React's virtual DOM.

In Hoplon, managing application state is outsourced to Clojurescript entirely. The application state is stored in vanilla Clojurescript data structures (atoms etc.), and Javelin provides a one-way data flow mechanism from those data structures whenever they are changed. You can use this data flow mechanism from Hoplon's DOM abstraction.

You could also use this data flow mechanism from any other DOM abstraction, as long as you have a way to coordinate changes between the abstraction and the DOM itself. JSX (or React.DOM) provides this, as well as the HLisp portion of Hoplon.

Differences in DOM Abstractions

Hoplon represents each HTML element as a native Javascript function that can be used to construct that element on the stateful DOM.

You are in a sense, simply deferring the stateful operations on the DOM, and instead working with the unevaluated DOM functions themselves. We'll call this the lazy DOM approach, as it is a kind of lazy evaluation of the DOM operations. You work with Clojurescript representations of HTML elements that are actually just compositions unevaluated DOM functions.

Each element is a function that uses the application state (or a subset of it) as input, and returns a composition of stateful DOM operations as the output.

Neither Hoplon nor React encourage you to work with the stateful DOM operations directly. To do so would be to take on the massive responsibility of managing the state yourself. That's exactly what these libraries are here to help with. With Hoplon you work with representations of DOM elements that are compositions of unevaluated DOM operations. This means that the DOM API is close at hand if you want to touch it.

With React, you only work with the virtual DOM abstraction, which separates you entirely from the native DOM operations. The conceptual framework that React offers excludes the native DOM entirely.

The proximity to the native DOM is one major reason for choosing Hoplon over React, or an interface to it.

It would be great to not depend on React, which is a massive codebase with a larger scope than we need.

Further Discussion of Hoplon's approach

Perhaps the most straight-forward comparison between Hoplon and React by Micha Niskin.

State:

Rules of Javelin Club:

Javelin can be used to create self-contained components (essentially state machines), which can be a great way to compose applications. Self-contained components with machine interfaces to each other.

For such a component, formula cells are the output (read-only) and the functions provided by the component are the input (write-only).

Many of these benefits come from the explicit dependency graph that Javelin uses.

Component Design

There is no need to pin ourselves to the semantics and APIs that HTML offers us.

The interface components that we build can be based around protocols, just like David Nolen demonstrates in CSP is Responsive Design and in his autocompleter post.

We can create our component interfaces by simply composing smaller interfaces together.

Design

Buttons: Icons or Text

Should we use icons or text for buttons?

Maybe we can use both. That way the English speaking users can be absolutely clear about the button's function, and non-english speakers will still be able to use the app before it is translated.

Guiding Principles

Interaction

The View Toolbar usually shows these items:

Some other ideas for items:

In order to keep the interface simple and uncluttered, an entry's toolbar is only shown when the entry is in focus. An entry is generally brought into focus by tapping on it, which causes a toolbar to appear beneath it.

When an entry is in focus, its toolbar shows actions that you can perform on that entry (e.g. move, add date, trash, etc.). The possible actions shown are different depending on the entry. For example, sub-entries and super-entries each have different default actions.

The possible actions may also change based on the current selection of the View Toolbar. For example, if you are on the DATE view, you will be presented with more date-oriented actions.

Any actions you perform on a super-entry are carried through to each of its sub-entries. For example, if you mark a super-entry for NOW, it marks all its sub-entries for NOW as well. If you move it, all its sub-entries move too.

Some remaining questions:

  1. Should the entry toolbar change when the view changes?
  2. Should the interaction mode change for each view? (e.g. a tap would edit an entry on one, and display the toolbar for another)

Probably "no" to both. The interaction should definitely stay consistent. The toolbar items can maybe change. Or maybe each entry has a button (or other UI element) whose functionality (and maybe look) changes when you change the view.

Other Possible Rearranging Interactions

Tap To Move

When you tap the move button, some arrows appear on (or below) that item. You can tap the arrows to move the entry in that direction. The entry stays right where it is in plance on the screen, and everything else moves around it. That way you can move it up or down repeatedly without chasing a moving tap target.

Maybe we should implement drag-and-drop as well, for people who are used to that.

Possible Entry Actions

These are all the possible actions tha you can perform on entries. Which ones appear on the entry toolbar is determined by the current view, and possibly the properties of the entry (like date).

  1. ADD: add entry to be topmost entry under current entry.
  2. MOVE: move entry to new location.
  3. DATE: add or adjust date
  4. REMINDER: add or adjust reminder
  5. NOW: Toggle NOW property (i.e. add/remove from NOW list)
  6. ARCHIVE: send entry to archive
  7. EDIT: edit current entry
  8. DELETE: permanently delete current entry

Archive

There is one default archive, by default named DONE You can create more archives and change their names if you want.

The archive is useful for reviewing your recent work. It should be presented in a manner that encourages that use.

In the archive, entries are grouped just as they were when you archived them. If the list that they are a part of is not archived, it will still show up in the archive (albeit it will look different) so that you know what project the tasks are a part of.

You can choose to send completed tasks to different archives if you set it up like that. Maybe you can specify an archive for each sublist.

Should the entries in the archive be listed in order of date completed by default? Should they be organized based on the groups that they were a part of when you completed them? Listing in the order completed is simpler, because we don't have to handle mismatches between the organization in TO DO and DONE. That's why we'll list them in order of date completed for now.

Animating Navigation

Besides navigating into and out of groups through tapping on group entries, you can also navigate out of groups by flicking up or down from within the group. If you flick your finger at the beginning or end of the scroll you can close the group, as if you were flicking away a picture in the Twitter iOS app.

There are a few ways to indicate that such a flick will perform that action as you scroll above and below the group in focus:

  1. The elements will start to animate as they would if you were to navigate out of the group.

  2. There is perhaps a little visual/tactical resistance (spring effect?) at the beginning and end of the scroll up and down in a group

There will be a slight zooming effect, but not as jarring as the default iOS 7+ zoom animation.

One way we can do it is to crossfade into a geometric representation of the relevant entries, then perform the zoom effect with the simple geometric shapes, and fade the entries back at the end. Maybe only the text fades out, and the bounding boxes are animated.

This should help give the person a sense of where they are in the app, and how they got there.

Master Group Entry

There will be a button in the master toolbar for accessing the settings.

Discussion on Whether Master Group Should Only Contain Groups

Should the master group only contain group entries?

If that were the case, the person would be forced to use groups. What if they wanted to delete all the group entries and just have everything in their master group? We should allow that. Even though it seems like it might not be useful, it might be useful to somebody for a purpose we cannot foresee.

Later, you will be able to create a view that displays only the entries of one group, and you can set that group to be your default view. You can do that to set it up so you, in practice, only have one list.

What would a view look like, if it is only a window into a group?

Maybe the entry toolbar would become the master entry toolbar of that window. You won't have delete or move actions, but you may have a settings action, where you can configure settings for that view/group.

Interactions Thread

Have a messaging-esque interface to Agenda

Screw SMS charges and character limit headaches. Have an in-app messaging-esque thread of communication between the user and the app.

The app might remind you of an upcoming appointment, and give you an option to remind you again closer to the date. You might be presented with a “remind me tommorow” button and a “don’t remind me again” button, or perhaps a date input control to specify exactly when you want to be reminded again. You respond as you wish to, and the app registers your response and updates itself accordingly. That interaction (the reminder, and your response) will remain in that thread, and you can go back and view it if you ever want to.

A few things could appear in the thread:

All of these items will be recorded and will persist in the thread, so that the user can go back and revist past interactions.

This can maybe be used in conjunction with native notifications. You get a notification in-app (and maybe you have some actions you can take on the notification view), and when you view it it disappears, but will show up and remain in the interactions thread.

The thread is an idea similar to (and partially inspired by) Slackbot from Slack.

Maybe the interactions thread (or maybe a different thread in the same vein) can show all the actions you've taken, with an opportunity to revert back to a past state. Batch undo in a sense.

Import/Export

Your data is not tied to us. You should be able to import your lists from Trello, Evernote, or whereever else, and export it right back.

You should be able to export your master list and all your settings to one file that you can email to yourself, delete the app, redownload the app, import the file, and have everything be the same.

Calendar and Timeline Views

Many ways calendars are usually divided are arbitrary and limiting.

We do often think about time in terms of weeks and months, but it's not necessarily the most effective way of viewing what you have scheduled.

The month view has many downsides, but has stuck around because that's what we associate with a calendar. The kind you can pin to the wall anyway.

The month view makes it hard to see anything on any day, and the division between one month and the next usually falls in a funny time of the week and doesn't mean much (besided rent being due).

Perhaps you can zoom in and out on the calendar view kind of like you can with the list view. Let's explore one way it could go down.

You start with the days in a grid seven wide for the days in a week. There is no division between the months, it's just a continuous grid you can scroll through. When you tap on a day, you zoom in to that day. You can scroll left and right to navigate between the days. If you scroll up or down, you can pop back out to the grid view. The action is the same resistant springy action that you get when zooming out in the list view.

Perhaps in the list view you can swipe left and right navigate between adjacent entries. Would that be too confusing because you're flipping the vertical placement into horizontal placement? Would the person feel lost?

Gestural Actions

Some simple gestural actions might be very helpful.

Swipe to complete a task? Maybe a soft swipe to mark it as complete and leave it in place, or a hard swipe to make it disappear immediately?

New Actions

Add ability to select multiple entries to perform actions on all of them.

NOTE: You can include a little dot or circle indicating that the entry is selected when you tap on it to bring up its toolbar. You can also use these dots to select multiple entries so that you can perform an action on them all.

Customization

Customization is key.

We will let users upload a CSS stylesheet to change the look and feel of their app.

We will also provide an interface for editing most parts of the app, including the functionality.

The interface for editing the app will look just like the app itself.

There may be an entry action group, for example, which you could add or remove entries from. Each entry represents an entry action. You can choose from several available ones.

People can make views for special uses they may have. They can customize the actions that appear in the entry toolbar, and perhaps even create their own actions.

Broader Ideas About Customization

In a certain sense, what we are really trying to build is a software system that will endure changing concerns and still be useful. Many of the issues that Steve Yegge discusses in The Pinnochio Problem are relevant to Agenda, even though our project is more limited in scope, and is intended for people to never have to program. One major point is that systems should never have to reboot. They should be able to sustain meaningful change without restarting.

Agenda is also intended to be highly visual, and it is ultimately just a tool for a limited thing. It is, to use the Yegge's language, ultimately more like a function than a real software system. It's use is extremely limited, and intentionally so. I don't intend for it to be a massive system for doing anything with a computer, just a system, essentially, for organizing bits of text over time and space (conceptual space, but often realized through the metaphor of physical space. Many of Yegge's reasons for building open-ended customizability into the root of the system are applicable to why we want to build customizabilty into our system.

However, in the context of Yegge's arguments, we should be careful with being very clear with what our scope is. Yegge's scope is virtually only limited by our interfaces with computers, and by their ulitmate physical nature as silicone automata. Our scope should be limited to the fact that there will only primarily be one person working on this project, and it would be better to build a great system for a small scope than to never finish building a system for a larger scope.

Timeline Views

It may be useful to view things on a timeline. Maybe have views for that?

Uncategorized Notes

Whenever you change the master list's view, the entry in focus remains in focus. If it won't appear anymore, the next closest entry (or closest ancestor/descendant) will end up in focus.

Super-entries stick at the top of the screen as you scroll through their sub-entries.

Text is large by default, but it gets smaller (until a minimum font size) as the text grows to fill the line. When it grows larger, it expands onto the next line.

Notes and Other Metadata

You can attach inert items to entries. We will refer to this as metadata. This is useful because you don't want to have to represent something as a task if it really isn't one. Sometimes you just want to put some information there for reference.

There are several types of metadata:

One type of metadata you can add is a text note. You can add add it to either an entry or a group entry and write anything you want.

Dates are another form of metadata. They can be due dates, dates for appointments, a time a task is scheduled for, or anything else.

Prototype 3: Agenda Moar

Collaboration

Pricing Model

Maybe $1 a month, maybe $5 a year.

Free download, free to use locally with all features. Including export and import, which means that you could export your data, email it to yourself, and edit it on another platform. That's fine. Totes chill.

Charge for cloud sync between devices, and for customization options.

Maybe when you build out image support, you can have different price tiers.

Images use more server resources. People who don't use images shuold be able to spend less, since text traffic and storage is dirt cheap.