@tristan2828/ui-foundation 4.31.0 → 4.33.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.
- package/conventions/AGENTS.md +2 -1
- package/conventions/docs/add-an-entity.md +13 -2
- package/conventions/docs/cell-patterns.md +8 -0
- package/conventions/docs/design-language.md +97 -6
- package/dist/components/app/data-table.d.ts +21 -1
- package/dist/components/app/data-table.js +20 -4
- package/dist/components/app/list-editor-rows.d.ts +18 -0
- package/dist/components/app/list-editor-rows.js +23 -0
- package/dist/components/app/list-editor.d.ts +25 -3
- package/dist/components/app/list-editor.js +61 -5
- package/dist/components/app/markdown.d.ts +10 -2
- package/dist/components/app/markdown.js +80 -11
- package/dist/components/app/milkdown-editor.d.ts +1 -1
- package/dist/components/app/milkdown-editor.js +19 -10
- package/dist/components/app/module-card.d.ts +53 -0
- package/dist/components/app/module-card.js +95 -0
- package/dist/components/app/prose-look.d.ts +26 -6
- package/dist/components/app/prose-look.js +24 -6
- package/dist/components/app/prose-table-scroll.d.ts +33 -0
- package/dist/components/app/prose-table-scroll.js +87 -0
- package/dist/components/app/rich-text-editor.d.ts +10 -0
- package/dist/components/app/rich-text-editor.js +5 -0
- package/dist/components/app/rich-text-node-views.d.ts +6 -3
- package/dist/components/app/rich-text-node-views.js +108 -5
- package/dist/components/app/widget-card.js +13 -4
- package/dist/components/ui/collapsible.d.ts +5 -0
- package/dist/components/ui/collapsible.js +12 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
- package/styles/index.css +14 -0
- package/styles/theme.css +5 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cn } from 'cn';
|
|
2
2
|
import { CALLOUT_KINDS } from '../../lib/markdown-callout.js';
|
|
3
3
|
import { CALLOUT_BOX, CALLOUT_LOOK } from './prose-look.js';
|
|
4
|
+
import { TABLE_SCROLL_FOCUS, watchTableScroll } from './prose-table-scroll.js';
|
|
4
5
|
// Each slot's key for its portal, for as long as the node view lives.
|
|
5
6
|
let made = 0;
|
|
6
7
|
const nextKey = () => `widget-${++made}`;
|
|
@@ -49,17 +50,55 @@ export function calloutView(node, getPos, registry) {
|
|
|
49
50
|
destroy: () => registry.remove(icon),
|
|
50
51
|
};
|
|
51
52
|
}
|
|
53
|
+
// A table's header row, as texts, from the node (its first row's cells).
|
|
54
|
+
function columnsOf(node) {
|
|
55
|
+
const header = node.firstChild;
|
|
56
|
+
if (!header)
|
|
57
|
+
return [];
|
|
58
|
+
const columns = [];
|
|
59
|
+
header.forEach((cell) => columns.push(cell.textContent));
|
|
60
|
+
return columns;
|
|
61
|
+
}
|
|
62
|
+
// How far an arrow key scrolls the table's box.
|
|
63
|
+
const SCROLL_STEP = 40;
|
|
64
|
+
// The keys a focused table box takes: the arrow keys, Home and End scroll
|
|
65
|
+
// it, as a box that isn't editable scrolls by itself, and any other plain
|
|
66
|
+
// key adds nothing (the text can't be typed into from the table's frame).
|
|
67
|
+
// Tab, Esc and shortcuts are left alone: Tab leaves the box, Esc and the
|
|
68
|
+
// shortcuts go on to the field. True when it took the key.
|
|
69
|
+
function scrollKey(box, event) {
|
|
70
|
+
if (event.ctrlKey || event.metaKey || event.altKey || event.key === 'Tab' || event.key === 'Escape')
|
|
71
|
+
return false;
|
|
72
|
+
const max = box.scrollWidth - box.clientWidth;
|
|
73
|
+
const to = {
|
|
74
|
+
ArrowLeft: box.scrollLeft - SCROLL_STEP,
|
|
75
|
+
ArrowRight: box.scrollLeft + SCROLL_STEP,
|
|
76
|
+
Home: 0,
|
|
77
|
+
End: max,
|
|
78
|
+
};
|
|
79
|
+
const target = to[event.key];
|
|
80
|
+
if (target !== undefined)
|
|
81
|
+
box.scrollLeft = Math.min(Math.max(target, 0), max);
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
// How long a press keeps the box out of the tab order: until the click it
|
|
86
|
+
// starts, or this long for one that never clicks (a touch that pans).
|
|
87
|
+
const PRESS_MS = 1000;
|
|
52
88
|
/**
|
|
53
89
|
* A table: in a box that scrolls sideways when it's wider than the text,
|
|
54
90
|
* with an "add a column" bar along its right and an "add a row" bar under
|
|
55
|
-
* it (widgets), Notion's way.
|
|
91
|
+
* it (widgets), Notion's way. While it scrolls, the box is a tab stop and
|
|
92
|
+
* a region named for its columns (prose-table-scroll.ts), which the arrow
|
|
93
|
+
* keys, Home and End scroll; a click in it still puts the caret in the
|
|
94
|
+
* cell clicked.
|
|
56
95
|
*/
|
|
57
|
-
export function tableView(getPos, registry) {
|
|
96
|
+
export function tableView(node, view, getPos, registry) {
|
|
58
97
|
const dom = document.createElement('div');
|
|
59
98
|
dom.dataset.slot = 'rich-text-table';
|
|
60
99
|
dom.className = 'group/table grid grid-cols-[minmax(0,1fr)_auto] gap-1';
|
|
61
100
|
const scroll = document.createElement('div');
|
|
62
|
-
scroll.className = 'overflow-x-auto rounded-lg border border-border';
|
|
101
|
+
scroll.className = cn('overflow-x-auto rounded-lg border border-border', TABLE_SCROLL_FOCUS);
|
|
63
102
|
const table = document.createElement('table');
|
|
64
103
|
const contentDOM = document.createElement('tbody');
|
|
65
104
|
table.append(contentDOM);
|
|
@@ -68,12 +107,76 @@ export function tableView(getPos, registry) {
|
|
|
68
107
|
const row = slot('col-start-1 flex h-5');
|
|
69
108
|
dom.append(scroll, column, row);
|
|
70
109
|
registry.set({ type: 'table', key: nextKey(), slot: row, column, getPos });
|
|
110
|
+
const region = watchTableScroll(scroll, table, columnsOf(node));
|
|
111
|
+
// A press in the box is the text's: a click in a cell puts the caret
|
|
112
|
+
// there. The browser focuses the nearest focusable ancestor of what was
|
|
113
|
+
// pressed, which would be the box while it's a tab stop, so the box
|
|
114
|
+
// leaves the tab order from the press to its click.
|
|
115
|
+
let pressing;
|
|
116
|
+
const released = () => {
|
|
117
|
+
clearTimeout(pressing);
|
|
118
|
+
pressing = undefined;
|
|
119
|
+
document.removeEventListener('click', releasedSoon, true);
|
|
120
|
+
region.resume();
|
|
121
|
+
};
|
|
122
|
+
const releasedSoon = () => setTimeout(released);
|
|
123
|
+
const onPointerDown = (event) => {
|
|
124
|
+
if (event.target === scroll && event.pointerType === 'mouse')
|
|
125
|
+
return; // its own scrollbar
|
|
126
|
+
region.suspend();
|
|
127
|
+
clearTimeout(pressing);
|
|
128
|
+
pressing = setTimeout(released, PRESS_MS);
|
|
129
|
+
document.addEventListener('click', releasedSoon, { capture: true, once: true });
|
|
130
|
+
};
|
|
131
|
+
scroll.addEventListener('pointerdown', onPointerDown);
|
|
132
|
+
// A key pressed while the box has focus is the box's (scrollKey), never
|
|
133
|
+
// ProseMirror's. It comes one of two ways. To the box itself, when the
|
|
134
|
+
// text's selection is outside it. Or, when the browser first moves
|
|
135
|
+
// focus to the editable text around it (as it can for a box inside
|
|
136
|
+
// editable text), to the text: then the box's blur to the text marks
|
|
137
|
+
// the key that follows in the same moment as the box's, and focus goes
|
|
138
|
+
// back to it.
|
|
139
|
+
const onBoxKey = (event) => {
|
|
140
|
+
if (event.target === scroll && scrollKey(scroll, event))
|
|
141
|
+
event.stopPropagation();
|
|
142
|
+
};
|
|
143
|
+
let bounced = false;
|
|
144
|
+
const onBlur = (event) => {
|
|
145
|
+
if (event.relatedTarget !== view.dom)
|
|
146
|
+
return;
|
|
147
|
+
bounced = true;
|
|
148
|
+
setTimeout(() => (bounced = false));
|
|
149
|
+
};
|
|
150
|
+
const onKeyDown = (event) => {
|
|
151
|
+
if (!bounced)
|
|
152
|
+
return;
|
|
153
|
+
bounced = false;
|
|
154
|
+
scroll.focus({ preventScroll: true });
|
|
155
|
+
if (scrollKey(scroll, event))
|
|
156
|
+
event.stopImmediatePropagation();
|
|
157
|
+
};
|
|
158
|
+
scroll.addEventListener('keydown', onBoxKey);
|
|
159
|
+
scroll.addEventListener('blur', onBlur);
|
|
160
|
+
view.dom.addEventListener('keydown', onKeyDown, true);
|
|
71
161
|
return {
|
|
72
162
|
dom,
|
|
73
163
|
contentDOM,
|
|
74
|
-
update: (next) =>
|
|
164
|
+
update: (next) => {
|
|
165
|
+
if (next.type.name !== 'table')
|
|
166
|
+
return false;
|
|
167
|
+
region.setColumns(columnsOf(next));
|
|
168
|
+
return true;
|
|
169
|
+
},
|
|
75
170
|
stopEvent: (event) => row.contains(event.target) || column.contains(event.target),
|
|
76
171
|
ignoreMutation: outside(contentDOM),
|
|
77
|
-
destroy: () =>
|
|
172
|
+
destroy: () => {
|
|
173
|
+
released();
|
|
174
|
+
region.destroy();
|
|
175
|
+
scroll.removeEventListener('pointerdown', onPointerDown);
|
|
176
|
+
scroll.removeEventListener('blur', onBlur);
|
|
177
|
+
scroll.removeEventListener('keydown', onBoxKey);
|
|
178
|
+
view.dom.removeEventListener('keydown', onKeyDown, true);
|
|
179
|
+
registry.remove(row);
|
|
180
|
+
},
|
|
78
181
|
};
|
|
79
182
|
}
|
|
@@ -21,6 +21,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
21
21
|
// muted note ("2 h ago") and an optional trailing element (a badge, a
|
|
22
22
|
// switch). A trailing control is labelled by the row's text: pass a
|
|
23
23
|
// function and it gets the id to put in `aria-labelledby`.
|
|
24
|
+
//
|
|
25
|
+
// Under data-density="compact" (#224) the card's padding drops to 12px and
|
|
26
|
+
// a row's to 4px, for a dense hub; text stays at type-body. On a phone a
|
|
27
|
+
// compact row is 44px tall and its link fills that height, so it stays a
|
|
28
|
+
// finger's target.
|
|
24
29
|
import { useId } from 'react';
|
|
25
30
|
import { Link } from 'react-router';
|
|
26
31
|
import { ExternalLinkIcon } from 'lucide-react';
|
|
@@ -34,11 +39,14 @@ export function WidgetCard({ title, icon: Icon, headingLevel = 2, link, isLoadin
|
|
|
34
39
|
const headingId = useId();
|
|
35
40
|
const Heading = `h${headingLevel}`;
|
|
36
41
|
const state = error ? 'error' : isLoading ? 'loading' : isEmpty ? 'empty' : 'success';
|
|
37
|
-
return (_jsx("section", { "aria-labelledby": headingId, "data-slot": "widget-card", "data-state": state, className: cn('min-w-0 self-start', className), children: _jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex items-center justify-between gap-3", children: [_jsxs(Heading, { id: headingId, className: "flex min-w-0 items-center gap-2 type-section-title text-foreground", children: [Icon && _jsx(Icon, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: title })] }), link && (_jsx(Link, { to: link.to, "aria-describedby": headingId, className: HEADER_LINK_CLASS, children: link.label }))] }), _jsx(CardContent, { "aria-busy": state === 'loading' || undefined, children: error ? (_jsx(ErrorState, { error: error, onRetry: onRetry, size: "compact" })) : state === 'loading' ? (_jsx(WidgetListSkeleton, { rows: skeletonRows })) : state === 'empty' ? (_jsx("p", { "data-state": "empty", className: "py-2 type-body text-muted-foreground", children: emptyMessage })) : (children) })] }) }));
|
|
42
|
+
return (_jsx("section", { "aria-labelledby": headingId, "data-slot": "widget-card", "data-state": state, className: cn('min-w-0 self-start', className), children: _jsxs(Card, { className: "density-compact:[--card-spacing:--spacing(3)]", children: [_jsxs(CardHeader, { className: "flex items-center justify-between gap-3", children: [_jsxs(Heading, { id: headingId, className: "flex min-w-0 items-center gap-2 type-section-title text-foreground", children: [Icon && _jsx(Icon, { "aria-hidden": "true", className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "truncate", children: title })] }), link && (_jsx(Link, { to: link.to, "aria-describedby": headingId, className: HEADER_LINK_CLASS, children: link.label }))] }), _jsx(CardContent, { "aria-busy": state === 'loading' || undefined, children: error ? (_jsx(ErrorState, { error: error, onRetry: onRetry, size: "compact" })) : state === 'loading' ? (_jsx(WidgetListSkeleton, { rows: skeletonRows })) : state === 'empty' ? (_jsx("p", { "data-state": "empty", className: "py-2 type-body text-muted-foreground", children: emptyMessage })) : (children) })] }) }));
|
|
38
43
|
}
|
|
39
44
|
// One row's height, shared by the skeleton and the list so the card doesn't
|
|
40
|
-
// jump: py-2 around one line of type-body (or a small control).
|
|
41
|
-
|
|
45
|
+
// jump: py-2 around one line of type-body (or a small control). Compact
|
|
46
|
+
// is 32px (6px around the line); on a phone it's 44px, the link's own
|
|
47
|
+
// padding making up the height (ROW_LINK_CLASS).
|
|
48
|
+
const ROW_CLASS = 'flex min-h-9 items-center gap-3 py-2 density-compact:min-h-8 density-compact:py-1 ' +
|
|
49
|
+
'max-sm:density-compact:min-h-11 max-sm:density-compact:py-0';
|
|
42
50
|
// Varied widths, so the skeleton reads as a list of titles, not a grid.
|
|
43
51
|
const SKELETON_WIDTHS = ['w-3/5', 'w-2/5', 'w-1/2', 'w-2/3', 'w-1/3', 'w-3/5'];
|
|
44
52
|
function WidgetListSkeleton({ rows }) {
|
|
@@ -49,7 +57,8 @@ export function WidgetList({ children, className }) {
|
|
|
49
57
|
return (_jsx("ul", { "data-slot": "widget-list", className: cn('flex flex-col divide-y divide-border', className), children: children }));
|
|
50
58
|
}
|
|
51
59
|
const ROW_LINK_CLASS = 'min-w-0 truncate rounded-sm font-medium text-foreground underline-offset-4 outline-none ' +
|
|
52
|
-
'hover:underline focus-visible:underline focus-visible:ring-3 focus-visible:ring-ring/50'
|
|
60
|
+
'hover:underline focus-visible:underline focus-visible:ring-3 focus-visible:ring-ring/50 ' +
|
|
61
|
+
'max-sm:density-compact:py-3';
|
|
53
62
|
export function WidgetListItem({ children, to, href, note, trailing, className }) {
|
|
54
63
|
const textId = useId();
|
|
55
64
|
const text = to !== undefined ? (_jsx(Link, { id: textId, to: to, title: children, className: ROW_LINK_CLASS, children: children })) : href !== undefined ? (_jsxs("a", { id: textId, href: href, target: "_blank", rel: "noreferrer", title: children, className: cn(ROW_LINK_CLASS, 'flex items-center gap-1'), children: [_jsx("span", { className: "truncate", children: children }), _jsx(ExternalLinkIcon, { "aria-hidden": "true", className: "size-3 shrink-0 text-muted-foreground" }), _jsx("span", { className: "sr-only", children: " (opens in a new tab)" })] })) : (_jsx("span", { id: textId, title: children, className: "min-w-0 truncate text-foreground", children: children }));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
|
|
2
|
+
declare function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props): import("react").JSX.Element;
|
|
3
|
+
declare function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props): import("react").JSX.Element;
|
|
4
|
+
declare function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props): import("react").JSX.Element;
|
|
5
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
|
|
3
|
+
function Collapsible({ ...props }) {
|
|
4
|
+
return _jsx(CollapsiblePrimitive.Root, { "data-slot": "collapsible", ...props });
|
|
5
|
+
}
|
|
6
|
+
function CollapsibleTrigger({ ...props }) {
|
|
7
|
+
return (_jsx(CollapsiblePrimitive.Trigger, { "data-slot": "collapsible-trigger", ...props }));
|
|
8
|
+
}
|
|
9
|
+
function CollapsibleContent({ ...props }) {
|
|
10
|
+
return (_jsx(CollapsiblePrimitive.Panel, { "data-slot": "collapsible-content", ...props }));
|
|
11
|
+
}
|
|
12
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { AppShell, type AppShellProps, type AppShellVersion, type NavEntry, type NavItem, type NavSection } from './components/app/app-shell.js';
|
|
2
2
|
export type { IconComponent } from './components/app/icon-component.js';
|
|
3
3
|
export { CountLink, type CountLinkProps } from './components/app/count-link.js';
|
|
4
|
-
export { DataTable, type DataTableProps, type DataTableWidth, type SortingState } from './components/app/data-table.js';
|
|
4
|
+
export { DataTable, type DataTableProps, type DataTableTotals, type DataTableWidth, type SortingState, } from './components/app/data-table.js';
|
|
5
5
|
export { type RowGroup } from './components/app/table-groups.js';
|
|
6
6
|
export { byDay, type ByDayOptions } from './lib/group-by-day.js';
|
|
7
7
|
export { EntityForm, type EntityFormProps } from './components/app/entity-form.js';
|
|
@@ -13,6 +13,7 @@ export { ErrorState } from './components/app/error-state.js';
|
|
|
13
13
|
export { InlineCreate, type InlineCreateProps } from './components/app/inline-create.js';
|
|
14
14
|
export { FoundationProviders } from './components/app/foundation-providers.js';
|
|
15
15
|
export { Markdown, type MarkdownProps } from './components/app/markdown.js';
|
|
16
|
+
export type { HeadingLevel } from './components/app/prose-look.js';
|
|
16
17
|
export { MockModeBanner } from './components/app/mock-mode-banner.js';
|
|
17
18
|
export { MultiChoice } from './components/app/multi-choice.js';
|
|
18
19
|
export { MultiPick, type MultiPickOption, type MultiPickProps } from './components/app/multi-pick.js';
|
|
@@ -36,6 +37,7 @@ export { ImageField, type ImageFieldProps } from './components/app/image-field.j
|
|
|
36
37
|
export { DEFAULT_IMAGE_TYPES, type ImageUpload, type ImageUploadResult } from './lib/image-file.js';
|
|
37
38
|
export { formatPlainDate, type PlainDate } from './lib/plain-date.js';
|
|
38
39
|
export { ChoiceCell, LinkCell, NameCell, type ChoiceCellProps, type LinkCellProps, type NameCellProps, } from './components/app/row-edit-cells.js';
|
|
40
|
+
export { ModuleCard, ModuleStrip, type CategorySlot, type ModuleCardLink, type ModuleCardPlace, type ModuleCardProps, type ModuleStripProps, } from './components/app/module-card.js';
|
|
39
41
|
export { WidgetCard, WidgetList, WidgetListItem, type WidgetCardProps, type WidgetListItemProps, } from './components/app/widget-card.js';
|
|
40
42
|
export { ThemeProvider } from './components/theme-provider.js';
|
|
41
43
|
export { LoginRoute, type LoginRouteProps } from './routes/login.js';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Composites
|
|
6
6
|
export { AppShell } from './components/app/app-shell.js';
|
|
7
7
|
export { CountLink } from './components/app/count-link.js';
|
|
8
|
-
export { DataTable } from './components/app/data-table.js';
|
|
8
|
+
export { DataTable, } from './components/app/data-table.js';
|
|
9
9
|
export {} from './components/app/table-groups.js';
|
|
10
10
|
export { byDay } from './lib/group-by-day.js';
|
|
11
11
|
export { EntityForm } from './components/app/entity-form.js';
|
|
@@ -40,6 +40,7 @@ export { ImageField } from './components/app/image-field.js';
|
|
|
40
40
|
export { DEFAULT_IMAGE_TYPES } from './lib/image-file.js';
|
|
41
41
|
export { formatPlainDate } from './lib/plain-date.js';
|
|
42
42
|
export { ChoiceCell, LinkCell, NameCell, } from './components/app/row-edit-cells.js';
|
|
43
|
+
export { ModuleCard, ModuleStrip, } from './components/app/module-card.js';
|
|
43
44
|
export { WidgetCard, WidgetList, WidgetListItem, } from './components/app/widget-card.js';
|
|
44
45
|
export { ThemeProvider } from './components/theme-provider.js';
|
|
45
46
|
// Screens outside the app shell
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.33.0",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/styles/index.css
CHANGED
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
|
|
17
17
|
@custom-variant dark (&:is(.dark *));
|
|
18
18
|
|
|
19
|
+
/* Compact density beyond tables (design-language.md "Density"): a
|
|
20
|
+
composite's own rows and padding tighten under data-density="compact"
|
|
21
|
+
on any ancestor, the way the table's tokens do. `density-compact:py-1`.
|
|
22
|
+
Text never shrinks under it. */
|
|
23
|
+
@custom-variant density-compact (&:where([data-density="compact"], [data-density="compact"] *));
|
|
24
|
+
|
|
19
25
|
@theme inline {
|
|
20
26
|
--font-heading: var(--font-sans);
|
|
21
27
|
--font-sans: 'Geist Variable', sans-serif;
|
|
@@ -113,6 +119,14 @@
|
|
|
113
119
|
line-height: var(--type-caption-leading);
|
|
114
120
|
font-weight: var(--type-caption-weight);
|
|
115
121
|
}
|
|
122
|
+
/* Counts on a hub's cards: tabular figures, so numbers line up from card
|
|
123
|
+
to card and don't shift width as they change. */
|
|
124
|
+
@utility type-numeric {
|
|
125
|
+
font-size: var(--type-numeric-size);
|
|
126
|
+
line-height: var(--type-numeric-leading);
|
|
127
|
+
font-weight: var(--type-numeric-weight);
|
|
128
|
+
font-variant-numeric: tabular-nums;
|
|
129
|
+
}
|
|
116
130
|
@utility type-heading-1 {
|
|
117
131
|
font-size: var(--type-heading-1-size);
|
|
118
132
|
line-height: var(--type-heading-1-leading);
|
package/styles/theme.css
CHANGED
|
@@ -138,6 +138,9 @@
|
|
|
138
138
|
--type-caption-size: 0.75rem; /* counts, metadata, helper text */
|
|
139
139
|
--type-caption-leading: 1rem;
|
|
140
140
|
--type-caption-weight: 400;
|
|
141
|
+
--type-numeric-size: 1.375rem; /* a count on a hub's ModuleCard, in tabular figures */
|
|
142
|
+
--type-numeric-leading: 1.75rem;
|
|
143
|
+
--type-numeric-weight: 500;
|
|
141
144
|
/*
|
|
142
145
|
* Headings inside written text (a record's notes: <Markdown> and the
|
|
143
146
|
* rich-text editor), not the screen's own outline. Three clear steps
|
|
@@ -309,6 +312,8 @@
|
|
|
309
312
|
--type-label-leading: 1.625rem;
|
|
310
313
|
--type-caption-size: 0.9375rem;
|
|
311
314
|
--type-caption-leading: 1.25rem;
|
|
315
|
+
--type-numeric-size: 1.75rem;
|
|
316
|
+
--type-numeric-leading: 2.25rem;
|
|
312
317
|
--type-heading-1-size: 2.1875rem;
|
|
313
318
|
--type-heading-1-leading: 2.8125rem;
|
|
314
319
|
--type-heading-2-size: 1.5625rem;
|