@protobase/cli 0.63.5 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.63.5";
2
+ var version = "0.64.0";
3
3
  //#endregion
4
4
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/cli",
3
- "version": "0.63.5",
3
+ "version": "0.64.0",
4
4
  "description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -42,12 +42,12 @@
42
42
  "dependencies": {
43
43
  "@better-auth/core": "^1.7.12",
44
44
  "@hono/node-server": "^2.1.3",
45
- "@protobase/config": "0.63.5",
46
- "@protobase/layout": "0.63.5",
47
- "@protobase/query": "0.63.5",
48
- "@protobase/schema": "0.63.5",
49
- "@protobase/server": "0.63.5",
50
- "@protobase/ui": "0.63.5",
45
+ "@protobase/config": "0.64.0",
46
+ "@protobase/layout": "0.64.0",
47
+ "@protobase/query": "0.64.0",
48
+ "@protobase/schema": "0.64.0",
49
+ "@protobase/server": "0.64.0",
50
+ "@protobase/ui": "0.64.0",
51
51
  "@react-email/render": "^2.1.0",
52
52
  "@scalar/hono-api-reference": "^0.12.10",
53
53
  "@tailwindcss/vite": "^4.3.3",
@@ -9,6 +9,7 @@ The list page of each resource, with its columns, filters, charts, search, expor
9
9
  - [Email, phone and web links](../reference/ui-config.md#names-and-columns): Fields marked as email, phone or URL show as links and are edited with the matching phone keyboard and browser autofill.
10
10
  - [Markdown and long text](../reference/ui-config.md#names-and-columns): Long text is edited in a box that grows with it. Markdown fields have Write and Preview and show rendered on the record page.
11
11
  - [Enum labels and colours](../reference/ui-config.md#names-and-columns): Enum values get readable labels and badge colours that show everywhere the value appears, and badges can be drawn as arrows, as on a rating scale.
12
+ - [Value tones and currencies](../reference/ui-config.md#value-tones-and-currencies): An enum value's badge takes the tone or colour its view gives it, and a money field shows in the ISO 4217 currency its view names.
12
13
  - [Pinned title column](../reference/ui-config.md#names-and-columns): Columns size to their content, and on desktop the title column stays in place while the table scrolls sideways.
13
14
  - [Pinned columns](../reference/ui-config.md#names-and-columns): A column's menu pins it to the left edge of the table, and the browser keeps the pins for each list.
14
15
  - [Resizable columns](../reference/ui-config.md#names-and-columns): Dragging a table column header's right edge, or the arrow keys on it, makes that column wider or narrower, down to a usable minimum. The widths last until the page is left or reloaded.
@@ -46,11 +46,11 @@ import type { billingProfiles, invoices, paymentMethods, plans, subscriptions }
46
46
 
47
47
  // The views of the billing schema: names, labels, and the named actions the billing page's buttons run.
48
48
 
49
- export const plansView = view<typeof plans>('plans').title((r) => r.name).nav({ hidden: true }).fields((r) => ({ price: r.price.prefix('€').decimals(2) }))
49
+ export const plansView = view<typeof plans>('plans').title((r) => r.name).nav({ hidden: true }).fields((r) => ({ price: r.price.prefix('€').currency('EUR').decimals(2) }))
50
50
 
51
51
  export const subscriptionsView = view<typeof subscriptions>('subscriptions')
52
52
  .nav({ hidden: true })
53
- .fields((r) => ({ renews_at: r.renews_at.label('Renews on'), status: r.status.valueLabels({ active: 'Active', canceled: 'Canceled' }) }))
53
+ .fields((r) => ({ renews_at: r.renews_at.label('Renews on'), status: r.status.valueLabels({ active: 'Active', canceled: 'Canceled' }).valueColors({ active: 'green' }) }))
54
54
  .actions((a) => [
55
55
  a.update('cancel', { label: 'Cancel plan', set: { status: 'canceled' }, confirm: 'Your plan stays active until the end of the period.' }),
56
56
  a.link('upgrade', { label: 'Upgrade', href: 'mailto:sales@example.com?subject=Upgrade%20from%20plan%20{plan}' }),
@@ -68,7 +68,7 @@ export const paymentMethodsView = view<typeof paymentMethods>('payment_methods')
68
68
  export const invoicesView = view<typeof invoices>('invoices')
69
69
  .title((r) => r.number)
70
70
  .names({ singular: 'Invoice', plural: 'Invoices' })
71
- .fields((r) => ({ total: r.total.prefix('€').decimals(2), issued_at: r.issued_at.label('Date'), status: r.status.valueLabels({ open: 'Open', paid: 'Paid' }) }))
71
+ .fields((r) => ({ number: r.number.format('code'), total: r.total.prefix('€').currency('EUR').decimals(2), issued_at: r.issued_at.label('Date'), status: r.status.valueLabels({ open: 'Open', paid: 'Paid' }).valueColors({ paid: 'green' }) }))
72
72
  .list((r) => ({ columns: [r.number, r.issued_at, r.total, r.status] }))
73
73
  .actions((a) => [a.action('download', { label: 'Download PDF' })])
74
74
  ```
@@ -132,9 +132,11 @@ export const ordersView = view<typeof orders>('orders')
132
132
  },
133
133
  })
134
134
  .fields((r) => ({
135
+ number: r.number.format('code'),
135
136
  status: r.status.help('Where the order is in its life. Moving it forward can notify the customer.').format('badge')
136
- .valueLabels({ draft: 'Draft', confirmed: 'Confirmed', picking: 'Picking', shipped: 'Shipped', delivered: 'Delivered', cancelled: 'Cancelled' }),
137
- total: r.total.help('Order total including VAT.').prefix('€').decimals(2),
137
+ .valueLabels({ draft: 'Draft', confirmed: 'Confirmed', picking: 'Picking', shipped: 'Shipped', delivered: 'Delivered', cancelled: 'Cancelled' })
138
+ .valueColors({ confirmed: 'blue', picking: 'amber', shipped: 'violet', delivered: 'green', cancelled: 'red' }),
139
+ total: r.total.help('Order total including VAT.').prefix('€').currency('EUR').decimals(2),
138
140
  discount_percent: r.discount_percent.label('Discount').help('Percentage taken off the order total.').format('percent'),
139
141
  paid: r.paid.help('Set when the payment has been received.'),
140
142
  created_at: r.created_at.label('Created').format('absolute'),
@@ -95,7 +95,7 @@ export const repairsView = view<typeof repairs>('repairs')
95
95
  number: r.number.format('code'),
96
96
  customer_id: r.customer_id.label('Customer'),
97
97
  status: r.status.format('badge'),
98
- estimate: r.estimate.prefix('€').decimals(2),
98
+ estimate: r.estimate.prefix('€').currency('EUR').decimals(2),
99
99
  booked_on: r.booked_on.label('Booked on'),
100
100
  }))
101
101
  .list((r) => ({
@@ -26,6 +26,7 @@ export const orders = resource('orders')
26
26
 
27
27
  export const ordersView = view<typeof orders>('orders')
28
28
  .title((r) => r.number)
29
+ .fields((r) => ({ number: r.number.format('code'), status: r.status.valueColors({ paid: 'green', shipped: 'violet', cancelled: 'red' }) }))
29
30
  .layout((r) => [
30
31
  l.section('Order', [r.number, r.status, r.total, r.iban]),
31
32
  // the last 10 changes, newest first, with Show more for the rest
@@ -35,7 +35,7 @@ export const repairsView = view<typeof repairs>('repairs')
35
35
  number: r.number.format('code'),
36
36
  customer_id: r.customer_id.label('Customer'),
37
37
  status: r.status.format('badge'),
38
- estimate: r.estimate.prefix('€').decimals(2),
38
+ estimate: r.estimate.prefix('€').currency('EUR').decimals(2),
39
39
  booked_on: r.booked_on.label('Booked on'),
40
40
  }))
41
41
  .list((r) => ({
@@ -98,17 +98,36 @@ Field hints:
98
98
  | `.label(text)` | Instead of the name in words (`customer_id` would be "Customer" anyway). |
99
99
  | `.help(text, { when? })` | A line under the field on the record page and in forms. `when: 'editing'` shows it only while the record is edited; see [Help while editing](#help-while-editing). |
100
100
  | `.confirm(question)` | Asks `question` before a person changes the value in a form or a list cell, and Cancel keeps it. See [Confirming a change](#confirming-a-change). |
101
- | `.format(format)` | `badge` shows statuses as coloured badges, `code` sets identifiers in monospace, `percent` adds a % sign. On a text field, `email`, `phone` and `url` show the value as a link that writes to it (`mailto:`), calls it (`tel:`, its digits and a leading +) or opens it in a new tab (only an `http` or `https` address; anything else stays text), on the record page, in its sidebar and in the list's cells but the title's, which links to the record; forms edit it in an email, phone or web address input, which brings up the keyboard for it on a mobile screen and offers the browser's autofill. Another field type with one of them stops the server at startup. `relative` on a timestamp shows it as "5 minutes ago" or "in 2 hours" in lists and on the record page, with the exact time on hover; `absolute` and `compact` are accepted and carried in `/meta` for apps that render their own lists. <gap>The admin does not use the `absolute` and `compact` formats yet.</gap> |
101
+ | `.format(format)` | `badge` shows statuses as coloured badges, `code` sets identifiers in monospace, as the list's identifier column too, `percent` adds a % sign, and `signed` shows a whole number in a list with its sign, green above zero and red below, such as a stock change of `+12` or `−3`; it needs an integer field. On a text field, `email`, `phone` and `url` show the value as a link that writes to it (`mailto:`), calls it (`tel:`, its digits and a leading +) or opens it in a new tab (only an `http` or `https` address; anything else stays text), on the record page, in its sidebar and in the list's cells but the title's, which links to the record; forms edit it in an email, phone or web address input, which brings up the keyboard for it on a mobile screen and offers the browser's autofill. Another field type with one of them stops the server at startup. `relative` on a timestamp shows it as "5 minutes ago" or "in 2 hours" in lists and on the record page, with the exact time on hover; `absolute` and `compact` are accepted and carried in `/meta` for apps that render their own lists. <gap>The admin does not use the `absolute` and `compact` formats yet.</gap> |
102
102
  | `.multiline()` | A text of several lines, such as a description: forms edit it in a box that grows with the text, so the page scrolls rather than the box, and the record page's sections, a page's field blocks and a card's subtitle keep its line breaks (a card shows the first three lines); the record's sidebar shows it on one line. On the record page and the create form it takes the whole width of its section. Only a text field takes it; the list's table shows the text on one line. |
103
103
  | `.markdown()` | Markdown, such as a task's notes: forms edit its source in a box that grows with it, with a Write and Preview switch above it, and store it exactly as typed. The record page's sections and a page's field blocks show it rendered as GitHub does, line breaks kept; raw HTML shows as text, links and images keep only http(s), mailto, tel and relative addresses, and links open in a new tab. A card's subtitle shows its first three lines rendered, its links as text; the list's table, the record's sidebar and a card's other lines show its text on one line without the marks. On the record page and the create form it takes the whole width of its section. Only a text field takes it, and the renderer loads with the first markdown shown. |
104
- | `.prefix(text)`, `.suffix(text)`, `.decimals(n)` | Money and measures: `€` and `2` show `€1,234.50`, `.suffix(' m²')` shows `120 m²`. They show wherever the number does: records, cards, table cells, forms and the range filter. |
104
+ | `.prefix(text)`, `.suffix(text)`, `.decimals(n)` | Text around a number and its decimals: `€` and `2` show `€1,234.50`, `.suffix(' m²')` shows `120 m²`. They show wherever the number does: records, cards, table cells, forms and the range filter. A prefix is plain text, so a list does not show `€` as money; `.currency()` does. |
105
+ | `.currency(code)` | The currency a decimal is an amount of, as an ISO 4217 code such as `EUR`: the list shows it as money in that currency, in its table and its range filter. See [Value tones and currencies](#value-tones-and-currencies). |
105
106
  | `.icon(name)` | An icon before the value on a card's meta row: one of the names [actions](layouts.md#named-actions) take, such as `bed`, `calendar` or `cpu`, and above the value in a section of [key facts](#key-facts). Nowhere else shows it. |
106
107
  | `.valueLabels({ value: text })` | How an enum's values read: `r.method.valueLabels({ ideal: 'iDEAL' })`. Only the enum's values are accepted. A value without a label shows as stored (`direct_debit`), so label every value that should read as words. |
107
- | `.valueColors({ value: '#hex' })`, `.badgeShape('arrow')` | An enum's badges filled with a colour per value (the text turns white or black, whichever reads), and drawn as arrows pointing right, as on a rating scale. The property portal's energy label: `r.energy_label.format('badge').valueColors({ a: '#00a651', g: '#ed1c24' }).badgeShape('arrow')`. Only the enum's values are accepted. |
108
+ | `.valueColors({ value: tone or '#hex' })`, `.badgeShape('arrow')` | An enum's badges in a tone per value, or filled with a colour (the text turns white or black, whichever reads), and drawn as arrows pointing right, as on a rating scale. See [Value tones and currencies](#value-tones-and-currencies). The property portal's energy label: `r.energy_label.format('badge').valueColors({ a: '#00a651', g: '#ed1c24' }).badgeShape('arrow')`. Only the enum's values are accepted. |
108
109
  | `.widget(name, props?)` | Shows and edits the field with a [field type](plugins.md#field-types) of the project or a plugin, such as `r.status.widget('steps')` or `r.score.widget('rating', { max: 5 })`, or with the built-in [`members`](plugins.md#members), which picks an organization's members into a JSON array of user ids; `props` are plain data. Without it, or on a storage type it does not show, the field shows as it would without the hint. |
109
110
 
110
111
  An enum's value labels show everywhere the admin shows its values: badges, select options, list and related-table cells, filter options and chips, record titles, the sidebar and search results. They are part of the view in `/api/meta`, so an app with its own screens can show them too, and [`enumLabel(value, labels)`](https://docs.protobase.net/api/schema/functions/enumlabel/) from `@protobase/schema` reads a value the same way. A label for a value the enum does not have stops the server at startup, so renaming a value cannot leave a stale label behind.
111
112
 
113
+ ### Value tones and currencies
114
+
115
+ An enum value's badge takes the tone or colour its view gives it, and a money field shows in the ISO 4217 currency its view names. Nothing is read from the names of values or fields: a value the view gives no tone is `neutral`, and a decimal without a currency is a number.
116
+
117
+ `.valueColors(...)` gives each value one of the badge tones, `neutral`, `blue`, `green`, `amber`, `red` or `violet`, or a hex colour that fills the badge. The tone shows on the value's badges in lists, boards and the record page, in the record page's heading and in a list's [related dots](#related-records-in-a-list); a hex colour colours a chart's group too. Anything else throws when the view is built.
118
+
119
+ `.currency('EUR')` on a decimal field makes the list show it as money in that currency, with the currency's sign and grouping for the language, in the table and in its range filter. A currency on another field type stops the server at startup, as does `.format('signed')` on a field that is not an integer.
120
+
121
+ ```ts
122
+ view<typeof orders>('orders').fields((r) => ({
123
+ number: r.number.format('code'),
124
+ status: r.status.valueColors({ confirmed: 'blue', shipped: 'violet', delivered: 'green', cancelled: 'red' }),
125
+ total: r.total.prefix('€').currency('EUR').decimals(2),
126
+ }))
127
+ ```
128
+
129
+ <gap>The record page, forms, cards, charts and exports cannot show a field in its currency because reading `.currency()` outside lists is not implemented.</gap> They show the field's `.prefix()`, so a money field takes both, as above.
130
+
112
131
  The table's columns take their width from the first rows it shows, and keep it while it scrolls, filters and sorts: the `.title` column gets the largest share, other text a share as long as its values, and short values such as enums, booleans, numbers, amounts and dates their own width whole. A value cut short shows whole when hovered. On a desktop the title column is pinned to the left by default, with the column before it when it is the second, so it stays in view when the table scrolls sideways; the column menu unpins it, and pins any other column to the left edge, and the choice is kept in the browser for that list. A column of the table is made wider or narrower by dragging its header's right edge, or with the left and right arrow keys on that edge, down to 64 pixels; the other columns keep their width, a drag never sorts, and the widths last until the page is left or reloaded. The row menu stays at the table's right edge as it scrolls sideways. Embedded lists and a page's `Table` blocks size their columns the same way.
113
132
 
114
133
  ### Related records in a list
@@ -128,7 +147,7 @@ view<typeof objectives>('objectives').list((r, d, c) => ({
128
147
  | Option | |
129
148
  | --- | --- |
130
149
  | `resource`, `field`, `through`, `filter` | Which records each row shows, as a [related section](#related-records) finds them: those whose relation `field` holds the row's key, or, through a join resource, those `through`'s records pick, so a task can serve several objectives. |
131
- | `status` | An enum field of `resource`. Each dot is filled with the colour its value has in the resource's view (`.valueColors(...)`), else in the value's badge tone. Required. |
150
+ | `status` | An enum field of `resource`. Each dot is filled with the colour or tone its value has in the resource's view (`.valueColors(...)`), `neutral` without one. Required. |
132
151
  | `sort` | The dots' order, AIP-132 on `resource`. |
133
152
  | `label` | The column's header; default the name in words. The name must not be a field of the resource. |
134
153
 
@@ -211,6 +230,7 @@ export const productsView = view<typeof products>('products')
211
230
 
212
231
  export const invoicesView = view<typeof invoices>('invoices')
213
232
  .title((r) => r.number)
233
+ .fields((r) => ({ number: r.number.format('code') }))
214
234
  .layout((r) => [
215
235
  l.section('Invoice', [r.number, r.customer]),
216
236
  l.related('Items', { resource: 'invoice_items', field: 'invoice_id', sort: 'position' }),
@@ -904,7 +924,7 @@ export const unitsView = view<typeof units>('units')
904
924
  property_id: r.property_id.label('Property'),
905
925
  area_m2: r.area_m2.label('Area').help('Living area in m².').suffix(' m²'),
906
926
  energy_label: r.energy_label.format('badge'),
907
- base_rent: r.base_rent.label('Market rent').help('What a new lease would ask today, per month.').prefix('€').decimals(2),
927
+ base_rent: r.base_rent.label('Market rent').help('What a new lease would ask today, per month.').prefix('€').currency('EUR').decimals(2),
908
928
  status: r.status.help('Let while a lease runs; vacant or under renovation between leases.').format('badge').valueLabels({ let: 'Let', vacant: 'Vacant', renovation: 'Renovation' }),
909
929
  }))
910
930
  .list((r) => ({ columns: [r.label, r.property_id, r.status, r.bedrooms, r.area_m2, r.energy_label, r.base_rent], search: [r.label] }))
@@ -1026,9 +1046,11 @@ export const ordersView = view<typeof orders>('orders')
1026
1046
  },
1027
1047
  })
1028
1048
  .fields((r) => ({
1049
+ number: r.number.format('code'),
1029
1050
  status: r.status.help('Where the order is in its life. Moving it forward can notify the customer.').format('badge')
1030
- .valueLabels({ draft: 'Draft', confirmed: 'Confirmed', picking: 'Picking', shipped: 'Shipped', delivered: 'Delivered', cancelled: 'Cancelled' }),
1031
- total: r.total.help('Order total including VAT.').prefix('€').decimals(2),
1051
+ .valueLabels({ draft: 'Draft', confirmed: 'Confirmed', picking: 'Picking', shipped: 'Shipped', delivered: 'Delivered', cancelled: 'Cancelled' })
1052
+ .valueColors({ confirmed: 'blue', picking: 'amber', shipped: 'violet', delivered: 'green', cancelled: 'red' }),
1053
+ total: r.total.help('Order total including VAT.').prefix('€').currency('EUR').decimals(2),
1032
1054
  discount_percent: r.discount_percent.label('Discount').help('Percentage taken off the order total.').format('percent'),
1033
1055
  paid: r.paid.help('Set when the payment has been received.'),
1034
1056
  created_at: r.created_at.label('Created').format('absolute'),