A Catalog Entity, an item for short, is something you refer to again and again, with values that should stay the same each time: a food and its nutrients, an exercise and the muscles it works, a book and its pages. It is a definition, not a series: you never log an item. You log an Entry on a Trackable that names an item, and the item’s values are read through the entry. Things that change over time are trackables; things you keep referring to are items. Nothing in the catalog code is about food: Food, Exercise, Books, Emotion and Activity are all ordinary catalogs.

A catalog

A catalog owns the fields every one of its items carries (attributes_schema, a record of number, text, tag, Category and yes/no fields) and the basis unit its items are measured in (base_unit: g for Food, rep for Exercise, none for Books). Items inherit both through the catalog’s id, so a few hundred bundled foods carry no copies of one shape, and no item can name its field kcal and drop out of a calorie total. A catalog can also name a bundled dataset its page imports from (dataset, a stable key). A dataset’s full bundle stays with the tooling (scripts/db/*.generated.json) as the source of a hand-pruned starter set (pnpm run db:prune, scripts/db/prune-catalogs.ts), which ships with the app (packages/core/src/*.starter.json): setup and the demo profiles install it, and the catalog page’s Import offers it again, skipping names already present. A catalog can say where its items come from in words (source, shown under its name), and carry an icon and a colour. Its name is unique and is its address, /catalog/<name>.

An item

  • Name, catalog and source: bundled (installed from a dataset or a module), user (made by the person) or oneoff (see below).
  • Values (attributes), each under one of its catalog’s fields. Every save checks them against the catalog: a value under a field it does not declare, of the wrong kind, or a Category value its list does not have is refused (Catalog Categories).
  • Made of other items? Never: a recipe is a Template, its ingredients logged as its parts, so an item’s values are always its own or lent by the item it is based on.
  • Basis: its values are per one basis unit, its own (unit, a meal counted in servings in a gram catalog) or else its catalog’s.
  • Units (units): named amounts in basis units, such as a cup of 240 g. The first one is its serving.
  • Signs (signs): each number’s sign, About (~), At least (≥) or At most (≤); a number with none is Known. A value it lacks is unknown, not zero (Diet and Nutrition).
  • Based on (based_on): another item and an amount between them, whose values fill the ones this item lacks, as About (Diet and Nutrition).
  • Barcode (barcode), stored as one spelling (Reading a Label).

A one-off is an ordinary item in every respect except its source, oneoff (CatalogSource in packages/core/src/types.ts), which keeps it out of its catalog’s page and out of the picker’s list. It is an entry’s detail, a lunch out, rather than something to reuse; it stays reachable from its entry and among the items recently logged.

How an entry reads an item

An entry names an item in an item field, a record field of kind reference on one catalog: Diet’s Food, Set’s Exercise, Read’s Book, Cardio’s Activity.

An item field can carry the entry’s amount (amount), at most one per record: Diet’s Food logs 120 g of a food, Emotions’ Emotion an intensity on a scale of its own (Emotions). The entry stores the item with its quantity, its unit, and base, the amount in the item’s basis units, frozen when it is logged so that a later change to a unit cannot rewrite a past entry (Units & Dimensions).

An entry with an amount contains its item’s values scaled by base: 120 g of a food contains 120 times its calories per gram. A Reading maps to those names, so Daily Calories is one reference to Diet, its entries mapped to calories and summed by day (Readings, Derived Trackables). A Category filter reads the item’s own values (item.type). A value the item lacks makes the day a minimum (≥) and a guessed one an estimate (~). An item field without an amount (Set’s Exercise, Read’s Book, Cardio’s Activity) names its item for the entry’s label, the table’s facet and the picker, but a Reading does not reach the item’s values through it.

Reading an item’s values

catalogResolver.resolve(id) is the one way to read an item’s values. It returns an item’s stored values, and fills the values it lacks from the item it is based on, so reading the attributes column directly misses every lent value. resolveMany resolves a page of items off one load. Both read one memoized snapshot of the catalogs, loaded once however many calls wait on it, and dropped whenever an item changes, a load that change overtook included; changing an item also refreshes the day totals and derived series read through it, so correcting a food corrects every day it was eaten. Resolving is asynchronous, so a caller resolves before it computes rather than during a render, and a computed fill takes resolved values. marks(id) says which values are guesses, which are partial (kept as a minimum, partials) and which read 0 when absent.

Display basis

Stored values are per one basis unit, per gram for Food, which is exact and unreadable. Display scales them, for reading only: per 100 of the basis unit (the default), per serving (the item’s first unit), or per one basis unit. Storage and every total keep the per-basis number. Only a measured basis, a unit of a known dimension such as g or ml, scales: a count basis carries ratios, like an exercise’s bodyweight factor, which show as stored. Numbers scale and round (whole from 100, one decimal from 1, two below); text, tags and Categories pass through. The choice is one setting on the device for every catalog.

Making catalogs and items

A catalog is made and changed in its own form (/catalog/new, /catalog/configure, components/catalog/CatalogForm.tsx): its name, basis unit, colour and source, and its fields in the same fields editor a trackable’s fields use (components/fields/FieldsEditor.tsx), offering number, text, tags, Category and yes/no fields. Removing a field that items hold values for warns that the values stay stored and nothing will read them. A second catalog with the same name is refused.

An item is made and changed in a form generated from its catalog’s fields (components/catalog/CatalogEntityForm.tsx, at /catalog/create and /catalog/edit), the same form the log dialog opens for an item that is not in the catalog yet. A new item is either Entered or Computed from other members; the kind is fixed once it is saved. The food-specific parts of the form, the serving line and based on, are in The item form and the serving line.

The catalog page and picking an item

One table behind the page and the picker. useCatalogTable (components/catalog/useCatalogTable.tsx) is one useDataTable from the component library holding search, filters, sort, grouping and which fields show, so the page’s table and cards and the item picker share every control.

  • Search matches the name only.
  • Filters are facets on Category fields, by their values’ labels in the list’s order with “No value”, and on text fields, a tag field split into its values. A number field is never a facet.
  • Sorting is on the number as shown, so per serving sorts per serving. A Category that takes one value sorts in its list’s order; one that takes several does not sort.
  • Grouping goes two levels deep, by a Category, text, tag or yes/no field. The name, the basis and the number fields are not offered. An item whose Category takes several values sits under each of its values.

The catalog page (routes/catalog.$category.tsx) shows its items as Auto, Table, Cards or Scatter, one choice remembered on the device for every catalog; Scatter is offered where the catalog has two number fields (Catalog Scatter). On Auto it shows the table when its visible columns fit and cards when they do not: useFitsTable remembers the width the table needed the last time it overflowed, and forgets it when the visible columns change. The table sits in a pane no taller than the screen, scrolling both ways inside it with its header stuck, so the page never scrolls sideways on a phone. The page also holds Import from the catalog’s dataset (skipping names already present) and, for a measured basis, the display basis and To fill in (Diet and Nutrition).

The item picker (components/entries/ItemPicker.tsx) is the library’s DataPicker under its DataToolbar, with its Filter, Sort, Group and View menus, over that table. On a pointer it is a popover held to the room below or above its trigger, which inside a dialog passes container so it keeps the wheel (Jeliroco Components); on a touch screen it is a bottom sheet sized to what the keyboard leaves. An option shows the item’s name and the fields the View menu leaves on, by default the basis, the first number field and the catalog’s main Category, in the basis the catalog page uses. Items logged recently are marked and rank first until another sort is chosen, one-offs among them. Which fields show and the grouping are remembered per catalog on the device; the search and the filters start empty at each opening, and the search box takes focus. The trackable’s templates sit above the list as buttons, narrowed by the search, and “Not in the catalog…” below it, carrying what was typed. A catalog with a measured basis also offers Scan barcode beside the picker on the phone (Reading a Label).

Catalog Categories · Catalog Scatter · Templates · Diet and Nutrition · Exercise & Strength · Entry · Value Types · Data Model