@momoi-labs/kiso 0.12.0 → 0.13.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/README.md +6 -0
- package/kiso/docs/components/README.md +3 -0
- package/kiso/docs/components/card.md +1 -1
- package/kiso/docs/components/chip-input.md +6 -5
- package/kiso/docs/components/empty-state.md +2 -3
- package/kiso/docs/components/filter-input.md +146 -0
- package/kiso/docs/components/input.md +2 -2
- package/kiso/docs/components/lifecycle.md +80 -0
- package/kiso/docs/components/log-view.md +1 -1
- package/kiso/docs/components/search.md +2 -2
- package/kiso/docs/components/status-badge.md +62 -0
- package/kiso/docs/components/textarea.md +1 -1
- package/kiso/docs/evolution.md +1 -0
- package/kiso/docs/patterns/crud.md +27 -0
- package/kiso/docs/patterns/filtering.md +4 -1
- package/kiso/docs/patterns/list-detail.md +76 -0
- package/kiso/docs/tokens.md +5 -4
- package/kiso/ui.css +268 -23
- package/package.json +1 -1
- package/tokens/build/tokens.d.ts +1 -1
- package/tokens/build/tokens.scss +1 -1
package/README.md
CHANGED
|
@@ -74,3 +74,9 @@ to open it locally, or `npm run build:gallery` to produce the static site.
|
|
|
74
74
|
|
|
75
75
|
See [Publishing Kiso](docs/publishing.md) for the first npm publication, trusted
|
|
76
76
|
publisher setup, release PRs, and version tags.
|
|
77
|
+
|
|
78
|
+
Run `npx playwright install chromium` once, then `npm run check:browser` to
|
|
79
|
+
check touch targets, taps, disabled controls, and keyboard interaction for
|
|
80
|
+
Lifecycle, Checkbox, Switch, Tabs, and TimeRangeControl in Chromium. Checks
|
|
81
|
+
cover coarse and fine pointers at 390px and 1280px in both themes. Set
|
|
82
|
+
`SCREENSHOT_DIR=artifacts/touch-controls` to save screenshots.
|
|
@@ -41,11 +41,13 @@ behavioral reference where one exists.
|
|
|
41
41
|
- [Dot](dot.md): Adds a decorative status mark beside readable text.
|
|
42
42
|
- [DropdownMenu](dropdown-menu.md): Presents contextual actions anchored to a specific object or trigger.
|
|
43
43
|
- [EmptyState](empty-state.md): Replaces an empty collection with an explanation and optional next action.
|
|
44
|
+
- [FilterInput](filter-input.md): Turns typed conditions into editable field/operator/value chips, with IN lists and nested AND/OR groups.
|
|
44
45
|
- [KV](kv.md): Describes one object through named facts.
|
|
45
46
|
- [LogView](log-view.md): Displays scrollable log output with follow-tail behavior.
|
|
46
47
|
- [Search](search.md): Filters visible content such as a list or table.
|
|
47
48
|
- [Sparkline](sparkline.md): Draws the shape of one metric series at cell size.
|
|
48
49
|
- [Stat](stat.md): Presents a named metric with optional change and context.
|
|
50
|
+
- [StatusBadge](status-badge.md): Says a record's state in one of three tones, pulsing while work goes.
|
|
49
51
|
- [StepBar](step-bar.md): Summarises a run as one segment per step, coloured by state.
|
|
50
52
|
- [StepList](step-list.md): Lists a run's steps with a state, a label, a timing, and an output.
|
|
51
53
|
- [Table / DataTable](table.md): Presents structured records with optional sorting, selection, filtering, and pagination.
|
|
@@ -60,6 +62,7 @@ behavioral reference where one exists.
|
|
|
60
62
|
- [BrandMark](brand-mark.md): Decorative letter or icon beside a product name.
|
|
61
63
|
- [Breadcrumb](breadcrumb.md): Shows the current location within a hierarchy.
|
|
62
64
|
- [Header](header.md): Composes persistent application navigation and global actions.
|
|
65
|
+
- [Lifecycle](lifecycle.md): Groups a detail screen's status, verbs, and destructive action in one row.
|
|
63
66
|
- [Navigation](navigation.md): Provides a generic semantic container for destination links.
|
|
64
67
|
- [PageHeader](page-header.md): Composes a page title, optional subtitle, and page-scoped action Buttons.
|
|
65
68
|
- [Pagination](pagination.md): Moves through known pages while exposing the current position.
|
|
@@ -80,7 +80,7 @@ it, so the mark points at the corner without touching it.
|
|
|
80
80
|
| Token | Value | Meaning |
|
|
81
81
|
| --- | --- | --- |
|
|
82
82
|
| `--color-corner-mark` | `--color-border-strong` | Tick colour. |
|
|
83
|
-
| `--corner-mark` | `1` |
|
|
83
|
+
| `--corner-mark` | `1` | Marks on (`1`) or off (`0`). Off marks also drop their inset, so they add no overflow to a scroll container around the panel. |
|
|
84
84
|
| `--corner-mark-tick` | 4px | Length of one tick. |
|
|
85
85
|
| `--corner-mark-gap` | 2px | Distance from tick end to the frame. |
|
|
86
86
|
|
|
@@ -25,6 +25,7 @@ which options a chip accepts. ChipInput never parses.
|
|
|
25
25
|
| One value from a known set | [Select](select.md) | Single choice. |
|
|
26
26
|
| Free text that is not a list | [Input](input.md) or [Textarea](textarea.md) | Nothing to chip. |
|
|
27
27
|
| Filtering what is already on screen | [Search](search.md) | A query, not stored values. |
|
|
28
|
+
| Filtering by fields, operators, and logical groups | [FilterInput](filter-input.md) | Typed conditions with IN lists and AND/OR groups. |
|
|
28
29
|
| Running a global action | [CommandPalette](command-palette.md) | Commands, not data. |
|
|
29
30
|
|
|
30
31
|
## Anatomy
|
|
@@ -93,11 +94,11 @@ form column.
|
|
|
93
94
|
| hover | Quiet border emphasis on the box. A hovered version segment deepens its fill. | `--color-border-strong`, `--color-accent-surface`. |
|
|
94
95
|
| focus | One ring around the whole box, never around the bare input. | `--color-ring`. |
|
|
95
96
|
| editing a segment | The value or the options become an input sized to their content; the chip takes an accent border and a confirm control appears. | `--color-primary`, `--color-accent-surface`. |
|
|
96
|
-
| no options | Only the `+` segment remains, in the
|
|
97
|
+
| no options | Only the `+` segment remains, in the muted foreground. | `--color-muted-foreground`. |
|
|
97
98
|
| chip invalid | The single chip is marked, not the field. Say why next to the field. | `--color-danger-surface`, `--color-danger-border`, `--color-danger`. |
|
|
98
99
|
| field invalid | `aria-invalid` on the box plus [ValidationMessage](validation-message.md). | `--color-danger`. |
|
|
99
100
|
| disabled | The box and every chip control are unavailable; chips stay readable. | `--color-disabled-surface`, `--color-disabled`. |
|
|
100
|
-
| empty | Placeholder in the input showing the shape of one entry. | `--color-
|
|
101
|
+
| empty | Placeholder in the input showing the shape of one entry. | `--color-muted-foreground`. |
|
|
101
102
|
|
|
102
103
|
An invalid chip and an invalid field are different failures. A version that
|
|
103
104
|
does not exist marks the chip; "add at least one dependency" marks the field.
|
|
@@ -156,7 +157,7 @@ ChipInput must have an explicit submit Button.
|
|
|
156
157
|
## Tokens
|
|
157
158
|
|
|
158
159
|
Box and input follow [Input](input.md): `--color-card`, `--color-input`,
|
|
159
|
-
`--color-border-strong`, `--color-foreground`, `--color-
|
|
160
|
+
`--color-border-strong`, `--color-foreground`, `--color-muted-foreground`,
|
|
160
161
|
`--color-ring`, `--color-disabled`, `--color-disabled-surface`, `--radius-md`,
|
|
161
162
|
`--shadow-xs`, `--size-control-md`, `--spacing-xs` padding,
|
|
162
163
|
`--motion-duration-fast` and `--motion-easing-standard`.
|
|
@@ -164,8 +165,8 @@ Box and input follow [Input](input.md): `--color-card`, `--color-input`,
|
|
|
164
165
|
Chips: `--color-secondary`, `--color-secondary-foreground`, `--radius-sm`,
|
|
165
166
|
`--size-control-sm`, `--type-size-label`, `--font-mono` and
|
|
166
167
|
`--type-size-metadata` for the segments, `--color-muted-foreground` for the
|
|
167
|
-
scope and
|
|
168
|
-
|
|
168
|
+
scope, option names, `=`, brackets and the empty `+`, `--color-border` for
|
|
169
|
+
the rules between segments,
|
|
169
170
|
`--color-card` for the value and option segments with `--color-accent-surface`
|
|
170
171
|
when hovered, `--color-foreground` for an option's value, `--color-link` for
|
|
171
172
|
the version,
|
|
@@ -56,7 +56,7 @@ error — errors are Alert. Do not color the whole EmptyState with status
|
|
|
56
56
|
tokens.
|
|
57
57
|
|
|
58
58
|
Surface tokens: text `--color-foreground` / `--color-muted-foreground` on
|
|
59
|
-
|
|
59
|
+
`--color-card` under the hatch that marks the region as not data. Icon uses
|
|
60
60
|
`--color-muted-foreground` unless it is purely decorative brand chrome.
|
|
61
61
|
|
|
62
62
|
## Sizes
|
|
@@ -121,8 +121,7 @@ PageHeader.
|
|
|
121
121
|
|
|
122
122
|
## Tokens
|
|
123
123
|
|
|
124
|
-
`--color-foreground`, `--color-muted-foreground`, `--color-
|
|
125
|
-
`--color-background`, optional icon `--color-muted-foreground`, `--spacing-lg`
|
|
124
|
+
`--color-foreground`, `--color-muted-foreground`, `--color-card`, optional icon `--color-muted-foreground`, `--spacing-lg`
|
|
126
125
|
padding (`--spacing-md` for `sm`), `--spacing-sm` gap, `--radius-lg` on the
|
|
127
126
|
icon frame, the hatch tokens (`--color-hatch`, `--hatch-line`,
|
|
128
127
|
`--hatch-period`, `--hatch-angle`), and
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# FilterInput
|
|
2
|
+
|
|
3
|
+
Build a structured search by typing. A complete condition becomes a chip with
|
|
4
|
+
three editable segments: field, operator, and value. `IN` values share one
|
|
5
|
+
segment. Parentheses visibly group chips, including nested groups.
|
|
6
|
+
|
|
7
|
+
Use FilterInput for searches that need typed comparisons or Boolean groups.
|
|
8
|
+
Use [Search](search.md) for a plain text query and [ChipInput](chip-input.md) for
|
|
9
|
+
structured lists such as dependencies. Existing ChipInput APIs and behavior do
|
|
10
|
+
not change.
|
|
11
|
+
|
|
12
|
+
## Ownership and API
|
|
13
|
+
|
|
14
|
+
The controlled `value` contains confirmed conditions and groups.
|
|
15
|
+
`onValueChange` receives each valid change. The product owns the field schema,
|
|
16
|
+
allowed operators and values, query execution, loading, pagination, and errors
|
|
17
|
+
from its data source. FilterInput never generates or executes SQL.
|
|
18
|
+
|
|
19
|
+
| Prop | Contract |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `label` | Required visible label, associated with the trailing input. |
|
|
22
|
+
| `fields` | Field definitions with `key`, optional `label`, `type`, optional `values`, `operators`, and `nullable`. |
|
|
23
|
+
| `value`, `onValueChange` | Controlled `FilterNode[]` and its change callback. |
|
|
24
|
+
| `onDraftChange` | Optional notification of trailing uncommitted text, including whole-expression editing. |
|
|
25
|
+
| `disabled` | Disables the input, suggestions, segment editing, connectors, removal, and actions. |
|
|
26
|
+
| `id`, `placeholder`, `className` | Input ID, optional syntax example, and root layout styling. |
|
|
27
|
+
|
|
28
|
+
Field keys are identifiers without spaces or syntax punctuation. Use `label`
|
|
29
|
+
for a friendly display name. `text` fields accept `=`, `!=`, `IN`, and
|
|
30
|
+
`CONTAINS`; `number` fields accept equality, inequality, ordering, and `IN`.
|
|
31
|
+
`nullable` adds `IS NULL`. `operators` restricts the field's supported operators.
|
|
32
|
+
`values` restricts values to the supplied list and supplies autocomplete.
|
|
33
|
+
Numbers must be finite; the product can apply additional domain constraints.
|
|
34
|
+
|
|
35
|
+
Conditions contain `kind: "condition"`, `field`, `operator`, `value`, and
|
|
36
|
+
`join`. Groups contain `kind: "group"`, `children`, and `join`. The first
|
|
37
|
+
node's join is ignored. Later nodes join the preceding expression with `AND`
|
|
38
|
+
or `OR`. Within each group, AND binds before OR. Explicit groups always keep
|
|
39
|
+
their parentheses. IN values are arrays, number values are numbers, and
|
|
40
|
+
IS NULL has a null value. An empty root means no filters; empty child groups
|
|
41
|
+
are invalid.
|
|
42
|
+
|
|
43
|
+
The separate `parseFilterExpression(text, fields)` helper returns either
|
|
44
|
+
`{ ok: true, value }` or `{ ok: false, error }`. It never returns a partially
|
|
45
|
+
parsed query. Pass `{ allowLeadingJoin: true }` only when appending to an
|
|
46
|
+
existing expression. `serializeFilterExpression` converts confirmed nodes back
|
|
47
|
+
to editable text. `getFilterSuggestions` exposes the same completion logic for
|
|
48
|
+
product adapters. These helpers do not alter ChipInput.
|
|
49
|
+
|
|
50
|
+
## Typing and syntax
|
|
51
|
+
|
|
52
|
+
| Input | Meaning |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `status=active` or `status:active` | Equality. |
|
|
55
|
+
| `status!=paused` | Inequality. |
|
|
56
|
+
| `lag>=100` | Numeric comparison; `>`, `<`, and `<=` also work. |
|
|
57
|
+
| `owner CONTAINS Mina` or `owner~Mina` | Text containment; matching rules belong to the product. |
|
|
58
|
+
| `region IN (eu, us)` | Any listed value. Brackets are also accepted. |
|
|
59
|
+
| `owner IS NULL` | Missing value for a nullable field. |
|
|
60
|
+
| `(status=active OR status=paused) AND region=eu` | Grouped conditions. |
|
|
61
|
+
|
|
62
|
+
Whitespace after a complete scalar condition creates a chip. Closing an IN
|
|
63
|
+
list or complete group also commits it. Enter confirms complete text without
|
|
64
|
+
a trailing delimiter. Adjacent conditions use AND. Operators and field keys
|
|
65
|
+
are case-insensitive; text values retain their case.
|
|
66
|
+
|
|
67
|
+
Quote spaces, reserved words, or commas inside a value, for example
|
|
68
|
+
`owner="Ana Silva"` or `owner IN ("Doe, Jane", Mina)`. Single and double quotes
|
|
69
|
+
work; backslash escapes protect quotes and backslashes. A trailing comma before
|
|
70
|
+
the list closes is ignored: `(eu, us, )` equals `(eu, us)`. Duplicate list
|
|
71
|
+
values collapse. Empty lists and missing values between commas remain invalid.
|
|
72
|
+
|
|
73
|
+
Incomplete text remains a draft. An unfinished group never commits only its
|
|
74
|
+
first condition. Invalid input displays a correction beside the field when
|
|
75
|
+
confirmation is attempted; it does not replace confirmed filters. Input method
|
|
76
|
+
composition does not create chips until composition ends. Editing inside the
|
|
77
|
+
draft does not auto-commit while the caret is away from its end.
|
|
78
|
+
|
|
79
|
+
## Editing and removal
|
|
80
|
+
|
|
81
|
+
Click a chip segment to edit only that field, operator, or value. Click a
|
|
82
|
+
group's opening parenthesis to edit that group's expression. Enter or Save
|
|
83
|
+
confirms; Escape or Cancel restores the saved value. Only one editor is active
|
|
84
|
+
at a time. Results continue to use the saved filter until confirmation.
|
|
85
|
+
|
|
86
|
+
Changing a scalar operator to IN wraps the existing value in an array. Changing
|
|
87
|
+
a multi-value IN to a scalar operator asks for a replacement value instead of
|
|
88
|
+
discarding extra values. Changing to IS NULL removes the value. Unsupported
|
|
89
|
+
field/operator/value combinations remain editable with a validation message.
|
|
90
|
+
|
|
91
|
+
Click a connector to switch AND/OR. Edit expression restores the entire query
|
|
92
|
+
as text so existing conditions can be regrouped. Enter saves that draft;
|
|
93
|
+
Escape cancels it. Clear filters removes both confirmed filters and drafts.
|
|
94
|
+
Removing the last condition in a group removes that empty group as well.
|
|
95
|
+
|
|
96
|
+
## Keyboard and accessibility
|
|
97
|
+
|
|
98
|
+
| Key or action | Behavior |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| ArrowDown / ArrowUp | Open or navigate contextual suggestions. |
|
|
101
|
+
| Tab | Accept a visible suggestion. With suggestions dismissed, continue normal focus navigation. |
|
|
102
|
+
| Shift+Tab | Move backward without accepting a suggestion. |
|
|
103
|
+
| Enter | Confirm valid text, or accept a suggestion for incomplete text. Never implicitly submit the surrounding form. |
|
|
104
|
+
| Escape | Dismiss suggestions; cancel a segment, group, or whole-expression edit. |
|
|
105
|
+
| Backspace in an empty trailing input | Restore the last condition or group as text for editing. |
|
|
106
|
+
| Click empty box space | Focus the trailing input. |
|
|
107
|
+
|
|
108
|
+
The trailing input is a labeled combobox. The listbox, active descendant, and
|
|
109
|
+
expanded state describe only visible suggestions. Chip controls are native
|
|
110
|
+
buttons with names that include their condition. Groups have accessible names.
|
|
111
|
+
Errors use `aria-invalid`, associated descriptions, and alert text. A polite
|
|
112
|
+
status announces committed changes. Confirmation restores focus to the edited
|
|
113
|
+
segment; removal returns it to the trailing input.
|
|
114
|
+
|
|
115
|
+
Provide an explicit product search action when results require submission. Do
|
|
116
|
+
not silently discard pending text on submit; observe `onDraftChange` or ask the
|
|
117
|
+
person to finish the draft first. This component does not persist filter state.
|
|
118
|
+
|
|
119
|
+
## Layout, states, and tokens
|
|
120
|
+
|
|
121
|
+
One continuous-typing interaction. Required states are empty, draft with
|
|
122
|
+
suggestions, confirmed chips, IN list, nested groups, editing, invalid draft,
|
|
123
|
+
and disabled. An empty result set belongs to the collection, not this control.
|
|
124
|
+
|
|
125
|
+
The box grows vertically as chips wrap. Groups wrap internally; they must not
|
|
126
|
+
force horizontal page scrolling on narrow screens. Touch targets use
|
|
127
|
+
`--size-touch-min` on coarse pointers. Desktop segments use
|
|
128
|
+
`--size-control-sm`. Maintain visible keyboard focus in both themes.
|
|
129
|
+
|
|
130
|
+
Compose the existing chip, input, button, and menu surfaces. Consume
|
|
131
|
+
`--color-card`, `--color-foreground`, `--color-muted-foreground`,
|
|
132
|
+
`--color-link`, `--color-border`, `--color-border-strong`,
|
|
133
|
+
`--color-accent-surface`, `--color-accent-surface-hover`, `--color-selected`,
|
|
134
|
+
`--color-selected-foreground`, `--color-ring`, and `--color-disabled`.
|
|
135
|
+
Use the existing spacing, radius, font, and shadow tokens. No new palette or
|
|
136
|
+
spacing scale is introduced.
|
|
137
|
+
|
|
138
|
+
## Boundaries
|
|
139
|
+
|
|
140
|
+
This is a small filter grammar, not SQL or a query-language interpreter. It
|
|
141
|
+
supports explicit Boolean groups but not NOT groups, field-to-field comparisons,
|
|
142
|
+
functions, or arbitrary SQL. The parser limits recursive nesting to 64 levels.
|
|
143
|
+
|
|
144
|
+
Validate the field schema and user filters again at the application boundary.
|
|
145
|
+
A server adapter maps allowed field identifiers and binds values as parameters.
|
|
146
|
+
Never concatenate editable values into executable SQL.
|
|
@@ -44,7 +44,7 @@ for large. It does not reduce text or target size below accessible product norms
|
|
|
44
44
|
|
|
45
45
|
| State | Behavior |
|
|
46
46
|
| --- | --- |
|
|
47
|
-
| Default | `--color-surface` background, `--color-border` outline, `--color-foreground` value; placeholder uses `--color-
|
|
47
|
+
| Default | `--color-surface` background, `--color-border` outline, `--color-foreground` value; placeholder uses `--color-muted-foreground`. |
|
|
48
48
|
| Hover | Border emphasis may increase without changing layout or implying focus. |
|
|
49
49
|
| Focus | Visible `--color-focus` ring; do not rely on border color alone. |
|
|
50
50
|
| Active | Native text selection and editing behavior; no separate persistent visual state. |
|
|
@@ -85,7 +85,7 @@ silently disable a field merely to show activity.
|
|
|
85
85
|
## Tokens
|
|
86
86
|
|
|
87
87
|
Use only semantic roles: `--color-surface`, `--color-foreground`,
|
|
88
|
-
`--color-
|
|
88
|
+
`--color-muted-foreground`, `--color-border`, `--color-focus`,
|
|
89
89
|
`--color-disabled`, and `--color-danger`; the five property-qualified body
|
|
90
90
|
typography tokens; `--spacing-xs`, `--spacing-sm`, `--spacing-md`, and
|
|
91
91
|
`--spacing-lg` as mapped above; `--radius-md`; `--shadow-sm` for elevated
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Lifecycle
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Lifecycle is the header row of a detail screen: what the record is, what you
|
|
6
|
+
can do to it, and the one action you do not want to hit by accident. It sits
|
|
7
|
+
to the right of the [PageHeader](page-header.md) inside `.between`. See
|
|
8
|
+
[List-detail](../patterns/list-detail.md#list-and-detail-as-separate-screens).
|
|
9
|
+
|
|
10
|
+
## Anatomy
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
Lifecycle (.lifecycle)
|
|
14
|
+
├── status cluster (role="group", "Status"): one or more StatusBadge
|
|
15
|
+
├── verbs cluster (role="group", "Actions", optional): Buttons
|
|
16
|
+
└── destructive action (optional): Button with .btn-danger-ghost
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
[ Running │ HTTP 200 ] [ Start │ Stop │ Restart ] Remove
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Status and verbs are two objects because they are different kinds of thing.
|
|
24
|
+
Each cluster gives its children's borders and corners to one frame, and a
|
|
25
|
+
hairline between children says they are separate readings or separate
|
|
26
|
+
choices. Status is read, so its cluster has no shadow and each badge keeps
|
|
27
|
+
its tone. The verbs are pressed, so their cluster carries the surface and the
|
|
28
|
+
shadow. The destructive action stands outside both with a wider gap, so a
|
|
29
|
+
slip on Restart cannot land on Delete.
|
|
30
|
+
|
|
31
|
+
## States
|
|
32
|
+
|
|
33
|
+
- **No verbs.** Pass no `actions` and the verbs cluster is not rendered. An
|
|
34
|
+
empty bordered box reads as something that failed to load.
|
|
35
|
+
- **Disabled verb.** A disabled Button keeps its place in the cluster with the
|
|
36
|
+
disabled surface. Start is off the whole time a thing runs; that is normal.
|
|
37
|
+
- **Work in progress.** The status cluster adds a pulsing
|
|
38
|
+
[StatusBadge](status-badge.md) that names the phase.
|
|
39
|
+
|
|
40
|
+
## Sizes
|
|
41
|
+
|
|
42
|
+
Buttons are `size="sm"`; the clusters are `--size-control-sm` tall. At 720px
|
|
43
|
+
and below the row takes the width under the title, the verbs take a row of
|
|
44
|
+
their own, and the destructive action follows without its extra gap.
|
|
45
|
+
|
|
46
|
+
## Accessibility
|
|
47
|
+
|
|
48
|
+
Each cluster is a `role="group"` named "Status" or "Actions". Every verb is
|
|
49
|
+
a text-labelled Button. The destructive action opens a confirmation; see
|
|
50
|
+
[Destructive actions](../patterns/destructive-actions.md). The focus ring of
|
|
51
|
+
a verb sits inside the cluster's edge.
|
|
52
|
+
|
|
53
|
+
## Tokens and implementation
|
|
54
|
+
|
|
55
|
+
Frames use `--color-border` and `--radius-md`; the verbs cluster uses
|
|
56
|
+
`--color-card` and `--shadow-xs`; hover uses `--color-accent-surface-hover`;
|
|
57
|
+
disabled uses `--color-disabled-surface` and `--color-disabled`. The
|
|
58
|
+
component takes slots, not data: the screen keeps its own conditions for
|
|
59
|
+
which verbs are disabled.
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
<div className="between">
|
|
63
|
+
<PageHeader><PageHeaderTitle>paperless</PageHeaderTitle></PageHeader>
|
|
64
|
+
<Lifecycle
|
|
65
|
+
status={<StatusBadge tone="success">Running</StatusBadge>}
|
|
66
|
+
actions={<><Button size="sm">Stop</Button><Button size="sm">Restart</Button></>}
|
|
67
|
+
destructive={<Button size="sm" variant="ghost" className="btn-danger-ghost">Remove</Button>}
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## When to use
|
|
73
|
+
|
|
74
|
+
- The header of a detail screen for a record with a state and verbs.
|
|
75
|
+
|
|
76
|
+
## When NOT to use
|
|
77
|
+
|
|
78
|
+
- A list screen. Its one primary verb goes in PageHeader `actions`.
|
|
79
|
+
- A create screen. There is nothing to start or delete yet.
|
|
80
|
+
- Row actions in a table. Use a [DropdownMenu](dropdown-menu.md).
|
|
@@ -35,7 +35,7 @@ No size variants. The product chooses the frame height. The frame uses
|
|
|
35
35
|
`--color-neutral-950`, text `--color-neutral-300`, `--color-border`,
|
|
36
36
|
`--radius-surface`, and `--spacing-md` padding. Log text uses `--font-mono`,
|
|
37
37
|
`--type-size-label`, and `--type-line-height-relaxed`. Timestamps use
|
|
38
|
-
`--color-neutral-
|
|
38
|
+
`--color-neutral-500` with `--spacing-sm` after them.
|
|
39
39
|
|
|
40
40
|
## States
|
|
41
41
|
|
|
@@ -50,7 +50,7 @@ FormField patterns.
|
|
|
50
50
|
| `submit` | Applies on Enter or an explicit "Search" Button. Good for expensive server queries. |
|
|
51
51
|
|
|
52
52
|
Appearance follows Input: `--color-surface`, `--color-border`,
|
|
53
|
-
`--color-foreground`, placeholder `--color-
|
|
53
|
+
`--color-foreground`, placeholder `--color-muted-foreground`. Leading icon
|
|
54
54
|
`--color-muted-foreground`.
|
|
55
55
|
|
|
56
56
|
Do not add a "global" variant — that is CommandPalette.
|
|
@@ -127,7 +127,7 @@ a different global binding.
|
|
|
127
127
|
## Tokens
|
|
128
128
|
|
|
129
129
|
Same semantic set as Input: `--color-surface`, `--color-foreground`,
|
|
130
|
-
`--color-
|
|
130
|
+
`--color-muted-foreground`, `--color-border`,
|
|
131
131
|
`--color-focus`, `--color-disabled`, `--spacing-sm` block and `--spacing-md`
|
|
132
132
|
inline padding, `--radius-md`, the five property-qualified body typography
|
|
133
133
|
tokens, `--motion-duration-fast`, and `--motion-easing-standard`.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# StatusBadge
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
StatusBadge says the state of one record in one of three tones, the way a
|
|
6
|
+
light on a device does. Green is alive: a running machine, a build that is
|
|
7
|
+
going, a run that ended well. Red is failed. Neutral is what is not
|
|
8
|
+
happening: pending, stopped, disabled. It is a [Badge](badge.md) with a
|
|
9
|
+
[Dot](dot.md); use Badge directly for classification that is not a state.
|
|
10
|
+
|
|
11
|
+
## Anatomy
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
StatusBadge (Badge)
|
|
15
|
+
├── Dot (decorative, pulses while work is going)
|
|
16
|
+
└── label (required)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## States
|
|
20
|
+
|
|
21
|
+
| Tone | Meaning | Badge variant |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `success` | Alive, or finished well. | `success` |
|
|
24
|
+
| `danger` | Failed. | `danger` |
|
|
25
|
+
| `neutral` | Not happening: pending, stopped, disabled. | `neutral` |
|
|
26
|
+
|
|
27
|
+
There is no fourth tone. "In progress" is not a colour: set `pulse` and let
|
|
28
|
+
the label name the phase ("Provisioning", "Building"), not "Running". A thing
|
|
29
|
+
that is merely up holds still.
|
|
30
|
+
|
|
31
|
+
## Sizes
|
|
32
|
+
|
|
33
|
+
One size, the Badge's. In a [Lifecycle](lifecycle.md) status cluster the
|
|
34
|
+
badge takes the cluster's height.
|
|
35
|
+
|
|
36
|
+
## Accessibility
|
|
37
|
+
|
|
38
|
+
The label carries the state; the dot is `aria-hidden`. Nothing depends on
|
|
39
|
+
colour alone. The pulse stops under `prefers-reduced-motion`, the same as
|
|
40
|
+
Dot's.
|
|
41
|
+
|
|
42
|
+
## Tokens and implementation
|
|
43
|
+
|
|
44
|
+
Tones map to Badge variants and reuse their tokens. The pulse is Dot's
|
|
45
|
+
`pulse`. No Radix primitive.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<StatusBadge tone="success">Running</StatusBadge>
|
|
49
|
+
<StatusBadge tone="success" pulse>Provisioning</StatusBadge>
|
|
50
|
+
<StatusBadge tone="danger">Failed</StatusBadge>
|
|
51
|
+
<StatusBadge tone="neutral">Stopped</StatusBadge>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## When to use
|
|
55
|
+
|
|
56
|
+
- A record's state in a table cell or in a detail screen's Lifecycle row.
|
|
57
|
+
|
|
58
|
+
## When NOT to use
|
|
59
|
+
|
|
60
|
+
- Classification without a state ("Beta", "v2"). Use Badge.
|
|
61
|
+
- A warning or information state. Use Badge `warning` or `info`, or an
|
|
62
|
+
[Alert](alert.md) when the person must act.
|
|
@@ -78,7 +78,7 @@ the loading status independently perceivable.
|
|
|
78
78
|
## Tokens
|
|
79
79
|
|
|
80
80
|
Use the same exact mapping as Input: `--color-surface`, `--color-foreground`,
|
|
81
|
-
`--color-
|
|
81
|
+
`--color-muted-foreground`, `--color-border`, `--color-focus`,
|
|
82
82
|
`--color-disabled`, `--color-danger`; all five body typography properties;
|
|
83
83
|
`--spacing-sm` block and `--spacing-md` inline padding; `--radius-md`;
|
|
84
84
|
`--motion-duration-fast`; and `--motion-easing-standard`. No primitive colors
|
package/kiso/docs/evolution.md
CHANGED
|
@@ -32,6 +32,7 @@ The growth model below is not theory. What it has produced so far:
|
|
|
32
32
|
| --- | --- | --- |
|
|
33
33
|
| **[ChipInput](components/chip-input.md)** | A self-hosted console had to collect a development image's dependencies: a list a person types, where every entry carries a backend, a version, and installer options. One form section per entry grew without limit and buried the list. | Added as a component contract with the segmented chip, the in-place value, and one segment per option. |
|
|
34
34
|
| **[StepList](components/step-list.md) and [StepBar](components/step-bar.md)** | The self-host console's Last run tab walks a machine through eleven lifecycle steps over several minutes and wrote its own list of steps with a state each, replacing a progress bar and a two-thousand-line log to search. An image build and the Host's own bootstrap were about to copy it. [#98](https://github.com/momoi-labs/blueprint/issues/98) records the evidence. | Added as two contracts: the list inside a Split with the step's LogView beside it, and the bar for a summary, a table cell, or a toast. |
|
|
35
|
+
| **[Lifecycle](components/lifecycle.md), [StatusBadge](components/status-badge.md) and the screen layout classes** | self-host settled one layout for list, detail and create screens (its ADR-0024), and quiet-inbox adopted it (its ADR 0010) by copying two components and about sixty lines of CSS. [#112](https://github.com/momoi-labs/blueprint/issues/112) records the evidence. | Added as two contracts, the `.list-filters`, `.lifecycle`, `.detail-tabs` and `.form-page` classes, and the separate-screens section of [List-detail](patterns/list-detail.md#list-and-detail-as-separate-screens). |
|
|
35
36
|
| **[Sparkline](components/sparkline.md)** | The self-host console collects metrics in-process, one sample per tick on an evenly spaced window, and needed the same shape three times over: a trend cell per application row, context under a KPI value, and one series per container. [#84](https://github.com/momoi-labs/blueprint/issues/84) records the evidence. | Added as a component contract and a Recharts-based component, single series, neutral by default, primary only for the series a tile is about. |
|
|
36
37
|
|
|
37
38
|
## Growth model: grow with real products
|
|
@@ -49,6 +49,33 @@ Discard restores the last saved values; Cancel leaves the task. Keep entries
|
|
|
49
49
|
on failure. Announce feedback in the message region, not around the buttons.
|
|
50
50
|
Immediate preferences and automatic filters do not require FormActions.
|
|
51
51
|
|
|
52
|
+
## Create screen
|
|
53
|
+
|
|
54
|
+
A record with a recipe is too much for a dialog, so it gets a screen: a
|
|
55
|
+
PageHeader that says what is about to be made, and the same form the detail
|
|
56
|
+
screen edits, in a `Card.form-page`. The card is at most 720px wide and the
|
|
57
|
+
footer's corners follow it. There is no Lifecycle row and no tabs; the
|
|
58
|
+
footer is FormActions with Cancel and the one primary verb. From 1024px,
|
|
59
|
+
when the card is a direct child of `.page` inside an AppShell, it takes the
|
|
60
|
+
height the header leaves and the form scrolls inside it, so pass `sticky` to
|
|
61
|
+
FormActions and it rests on the card's edge. Below 1024px, or with
|
|
62
|
+
`data-fill="false"` on the card, the document scrolls and the actions end the
|
|
63
|
+
form in flow. The detail screen
|
|
64
|
+
is in [List-detail](list-detail.md#list-and-detail-as-separate-screens).
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<PageHeader><PageHeaderTitle>Deploy an application</PageHeaderTitle></PageHeader>
|
|
68
|
+
<Card className="form-page">
|
|
69
|
+
<Form>
|
|
70
|
+
<div className="form-body">…</div>
|
|
71
|
+
<FormActions sticky>
|
|
72
|
+
<Button>Cancel</Button>
|
|
73
|
+
<Button type="submit" variant="primary">Deploy</Button>
|
|
74
|
+
</FormActions>
|
|
75
|
+
</Form>
|
|
76
|
+
</Card>
|
|
77
|
+
```
|
|
78
|
+
|
|
52
79
|
## Flow
|
|
53
80
|
|
|
54
81
|
### Create
|
|
@@ -28,6 +28,7 @@ invent a per-list filter kit.
|
|
|
28
28
|
| Toggle a single facet on/off | [Switch](../components/switch.md) or [Checkbox](../components/checkbox.md) | "Show only replicas with lag" |
|
|
29
29
|
| Multiple values from a set | [Checkbox](../components/checkbox.md) group inside a [Popover](../components/popover.md) | "Region: ☑ eu ☑ us ☐ ap" |
|
|
30
30
|
| Range or complex facet | [Popover](../components/popover.md) with form controls | "Lag: 0–500 ms" |
|
|
31
|
+
| Typed conditions across fields, with logical groups | [FilterInput](../components/filter-input.md) | `(status=active OR region IN (eu, us)) AND lag>100` |
|
|
31
32
|
| Quick toggles (few, stable) | [Button](../components/button.md) `ghost` toggle or [Tabs](../components/tabs.md) | "All / Active / Archived" |
|
|
32
33
|
| Active-filter summary | [Badge](../components/badge.md) per active filter or a text line | "Status: degraded ×" |
|
|
33
34
|
|
|
@@ -117,7 +118,9 @@ When filters exclude everything:
|
|
|
117
118
|
- Do not invent a filter UI that competes with [Select](../components/select.md)
|
|
118
119
|
/ [Checkbox](../components/checkbox.md) / [Popover](../components/popover.md).
|
|
119
120
|
If the facet is one-of-many, use Select; if many-of-many, use a Checkbox
|
|
120
|
-
group in a Popover.
|
|
121
|
+
group in a Popover. Use [FilterInput](../components/filter-input.md) when
|
|
122
|
+
people need typed conditions, per-field operators, or nested AND/OR groups.
|
|
123
|
+
Its chips already show and remove active filters; do not duplicate them as Badges.
|
|
121
124
|
- Filter state is part of the list's state, not global. Navigating away and
|
|
122
125
|
back may restore it (documented per surface) but filters must not leak into
|
|
123
126
|
unrelated lists.
|
|
@@ -101,9 +101,85 @@ Narrow (stacked detail):
|
|
|
101
101
|
└──────────────────────────────────────┘
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
## List and detail as separate screens
|
|
105
|
+
|
|
106
|
+
An alternative to the split: the list is one screen and each record opens a
|
|
107
|
+
detail screen of its own. Use it when a record carries more than a pane can
|
|
108
|
+
hold, such as a form, a log, or a terminal. Its create screen is in
|
|
109
|
+
[CRUD](crud.md#create-screen).
|
|
110
|
+
|
|
111
|
+
### List screen
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
<PageHeader actions={<Button size="sm" variant="primary">Deploy application</Button>}>
|
|
115
|
+
<PageHeaderTitle>Applications</PageHeaderTitle>
|
|
116
|
+
</PageHeader>
|
|
117
|
+
<div className="list-filters">
|
|
118
|
+
<Search aria-label="Search applications" />
|
|
119
|
+
<Select>…</Select>
|
|
120
|
+
{filtering ? <Button size="sm" variant="ghost">Clear filters</Button> : null}
|
|
121
|
+
</div>
|
|
122
|
+
<div className="table-wrap">
|
|
123
|
+
<Table>…</Table>
|
|
124
|
+
<div className="table-footer">3 of 12 applications</div>
|
|
125
|
+
</div>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- The one primary verb goes in PageHeader `actions`, not in the filter row.
|
|
129
|
+
Narrowing a list and adding to it are opposite intentions.
|
|
130
|
+
- `.list-filters`: Search first and growing, a Select sized to its content,
|
|
131
|
+
"Clear filters" only once something is filtered. Below 560px each control
|
|
132
|
+
takes the row.
|
|
133
|
+
- The count goes in `.table-footer`, not above the table.
|
|
134
|
+
- From 1024px, a `.table-wrap` that is the last child of `.page` fills the
|
|
135
|
+
height the header and filters leave: the rows scroll under the sticky
|
|
136
|
+
header and the count stays on the bottom edge, as a detail card does. Set
|
|
137
|
+
`data-fill="false"` on the `.table-wrap` to end the table with its last
|
|
138
|
+
row instead.
|
|
139
|
+
- A small record is created in a [Modal / Dialog](../components/modal-dialog.md).
|
|
140
|
+
|
|
141
|
+
### Detail screen
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
<div className="between">
|
|
145
|
+
<PageHeader><PageHeaderTitle>paperless</PageHeaderTitle></PageHeader>
|
|
146
|
+
<Lifecycle status={…} actions={…} destructive={…} />
|
|
147
|
+
</div>
|
|
148
|
+
<Card className="detail-tabs">
|
|
149
|
+
<Tabs defaultValue="configuration">
|
|
150
|
+
<TabsList aria-label="Application details">…</TabsList>
|
|
151
|
+
<TabsContent value="configuration">
|
|
152
|
+
<Form>
|
|
153
|
+
<div className="form-body">…</div>
|
|
154
|
+
<FormActions sticky>…</FormActions>
|
|
155
|
+
</Form>
|
|
156
|
+
</TabsContent>
|
|
157
|
+
<TabsContent value="logs" className="detail-logs"><LogView>…</LogView></TabsContent>
|
|
158
|
+
</Tabs>
|
|
159
|
+
</Card>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- The screen says its own name, and [Lifecycle](../components/lifecycle.md)
|
|
163
|
+
sits beside it. The destructive action lives in that row, not at the foot
|
|
164
|
+
of the form.
|
|
165
|
+
- One `Card.detail-tabs`, not a split. When the app reads facts back about
|
|
166
|
+
the record, a Summary tab comes first.
|
|
167
|
+
- `Form` does not render `.form-body`; write it. A panel whose only child is
|
|
168
|
+
the form gives its padding to `.form-body`, so a sticky
|
|
169
|
+
[FormActions](../components/form-actions.md) spans the panel.
|
|
170
|
+
- A panel with its own scroller, a [LogView](../components/log-view.md) or a
|
|
171
|
+
thread, takes `.detail-logs` or `.detail-pane` and reaches the card's edges.
|
|
172
|
+
- From 1024px, when the card is a direct child of `.page` inside an
|
|
173
|
+
[AppShell](../components/app-shell.md), the shell stops at the viewport
|
|
174
|
+
and the card takes the height the header leaves; each panel scrolls inside
|
|
175
|
+
it. Set `data-fill="false"` on the card to let the document scroll
|
|
176
|
+
instead. Below 1024px the document scrolls and an edge-to-edge panel is
|
|
177
|
+
60vh tall.
|
|
178
|
+
|
|
104
179
|
## When to use
|
|
105
180
|
|
|
106
181
|
- Collections where inspecting one item while keeping list context matters.
|
|
182
|
+
- Records with configuration and output of their own: use separate screens.
|
|
107
183
|
- Entities that share the same columns and detail shape.
|
|
108
184
|
|
|
109
185
|
## When NOT to use
|
package/kiso/docs/tokens.md
CHANGED
|
@@ -60,7 +60,7 @@ never text on dark.
|
|
|
60
60
|
| `elevated-surface` | Menus, popovers, and dialogs. | `dark.700` | `white` |
|
|
61
61
|
| `foreground` | Primary text and content that must carry the strongest hierarchy. | `dark.100` | `neutral.900` |
|
|
62
62
|
| `muted-foreground` | Secondary text and labels. It remains normal-text eligible. | `dark.400` | `neutral.600` |
|
|
63
|
-
| `subtle-foreground` |
|
|
63
|
+
| `subtle-foreground` | Large supporting text and non-text graphics where the surface provides 3:1 contrast; never small text. | `dark.450` | `neutral.500` |
|
|
64
64
|
| `border` | Dividers and control outlines; never text. | `dark.600` | `neutral.300` |
|
|
65
65
|
| `primary` | The primary fill: primary buttons, solid badges, checked controls, the brand mark. Not text-eligible on dark. | `#684bb5` | `#5b3fc4` |
|
|
66
66
|
| `accent` | Secondary emphasis and highlights, not the page's main action. | `accent.300` | `accent.800` |
|
|
@@ -143,8 +143,9 @@ terminals — which cannot follow `color-scheme`, so their text cannot either.
|
|
|
143
143
|
|
|
144
144
|
Use `foreground` for default reading, `muted-foreground` when content is
|
|
145
145
|
secondary but still needs normal-text contrast, and `subtle-foreground` only
|
|
146
|
-
for large or non-
|
|
147
|
-
|
|
146
|
+
for large supporting copy or non-text graphics on surfaces that provide at
|
|
147
|
+
least 3:1 contrast. Use `primary` for the fill of the action that drives the
|
|
148
|
+
current task; use `link` for anything that reads as a
|
|
148
149
|
link or an active indicator, because the fill is not text-eligible on dark.
|
|
149
150
|
Use `accent` to draw secondary attention without creating another primary
|
|
150
151
|
action.
|
|
@@ -286,7 +287,7 @@ points at the corner without touching it.
|
|
|
286
287
|
|
|
287
288
|
| Token | Value | Meaning |
|
|
288
289
|
| --- | --- | --- |
|
|
289
|
-
| `--corner-mark` | `1` |
|
|
290
|
+
| `--corner-mark` | `1` | Marks on (`1`) or off (`0`). Off marks also drop their inset, so they add no overflow to a scroll container around the panel. |
|
|
290
291
|
| `--corner-mark-tick` | 4px | Length of one tick. |
|
|
291
292
|
| `--corner-mark-gap` | 2px | Distance from tick end to the frame. |
|
|
292
293
|
|
package/kiso/ui.css
CHANGED
|
@@ -75,7 +75,7 @@ h1, h2, h3, h4, p, figure { margin: 0; }
|
|
|
75
75
|
font-weight: var(--type-weight-semibold);
|
|
76
76
|
letter-spacing: var(--type-letter-spacing-caps);
|
|
77
77
|
text-transform: uppercase;
|
|
78
|
-
color: var(--color-
|
|
78
|
+
color: var(--color-muted-foreground);
|
|
79
79
|
}
|
|
80
80
|
.t-mono, .mono, code, pre, kbd {
|
|
81
81
|
font-family: var(--font-mono);
|
|
@@ -106,9 +106,11 @@ a, .link {
|
|
|
106
106
|
a:hover, .link:hover { text-decoration: underline; }
|
|
107
107
|
|
|
108
108
|
/* --- layout helpers ------------------------------------------------------ */
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
.stack
|
|
109
|
+
/* One explicit column of minmax(0, 1fr), so a wide table, chart, or log line
|
|
110
|
+
inside a stack cannot widen the page; it scrolls or wraps in place instead. */
|
|
111
|
+
.stack { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--spacing-lg); }
|
|
112
|
+
.stack-sm{ display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--spacing-sm); }
|
|
113
|
+
.stack-xs{ display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--spacing-xs); }
|
|
112
114
|
.row { display: flex; align-items: center; gap: var(--spacing-sm); }
|
|
113
115
|
.row-md { display: flex; align-items: center; gap: var(--spacing-md); }
|
|
114
116
|
.row-wrap{ display: flex; align-items: center; gap: var(--spacing-sm); flex-wrap: wrap; }
|
|
@@ -220,11 +222,13 @@ a:hover, .link:hover { text-decoration: underline; }
|
|
|
220
222
|
.btn-primary[data-loading="true"]::after { border-color: var(--color-primary-foreground); border-top-color: transparent; }
|
|
221
223
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
222
224
|
|
|
223
|
-
/* Button group / segmented control
|
|
225
|
+
/* Button group / segmented control. `.btn-group` twice, so the shared corners
|
|
226
|
+
beat the appearance rule that rounds every control and comes later in this
|
|
227
|
+
file; otherwise each button keeps its own corners and the group falls apart. */
|
|
224
228
|
.btn-group { display: inline-flex; }
|
|
225
|
-
.btn-group .btn { border-radius: 0; border-color: var(--color-input); background: var(--color-card); }
|
|
226
|
-
.btn-group .btn:first-child { border-start-start-radius: var(--radius-md); border-end-start-radius: var(--radius-md); }
|
|
227
|
-
.btn-group .btn:last-child { border-start-end-radius: var(--radius-md); border-end-end-radius: var(--radius-md); }
|
|
229
|
+
.btn-group.btn-group .btn { border-radius: 0; border-color: var(--color-input); background: var(--color-card); }
|
|
230
|
+
.btn-group.btn-group .btn:first-child { border-start-start-radius: var(--radius-md); border-end-start-radius: var(--radius-md); }
|
|
231
|
+
.btn-group.btn-group .btn:last-child { border-start-end-radius: var(--radius-md); border-end-end-radius: var(--radius-md); }
|
|
228
232
|
.btn-group .btn + .btn { margin-inline-start: -1px; }
|
|
229
233
|
.btn-group .btn:hover { background: var(--color-accent-surface); }
|
|
230
234
|
.btn-group .btn[aria-pressed="true"] { background: var(--color-secondary); z-index: 1; }
|
|
@@ -355,7 +359,7 @@ fieldset.form-body { border: 0; margin: 0; }
|
|
|
355
359
|
transition: border-color var(--motion-duration-fast) var(--motion-easing-standard);
|
|
356
360
|
}
|
|
357
361
|
.textarea { height: auto; min-height: 76px; padding-block: var(--spacing-sm); line-height: var(--type-line-height-normal); resize: vertical; }
|
|
358
|
-
.input::placeholder, .textarea::placeholder { color: var(--color-
|
|
362
|
+
.input::placeholder, .textarea::placeholder { color: var(--color-muted-foreground); }
|
|
359
363
|
.input:hover, .select:hover, .textarea:hover { border-color: var(--color-border-strong); }
|
|
360
364
|
.input:disabled, .select:disabled, .textarea:disabled {
|
|
361
365
|
background: var(--color-disabled-surface);
|
|
@@ -393,10 +397,10 @@ fieldset.form-body { border: 0; margin: 0; }
|
|
|
393
397
|
}
|
|
394
398
|
|
|
395
399
|
/* chip input: several structured values in one field */
|
|
396
|
-
.chip-input { position: relative; display: grid; gap: var(--spacing-xs); }
|
|
400
|
+
.chip-input { position: relative; display: grid; min-width: 0; gap: var(--spacing-xs); }
|
|
397
401
|
.chip-input-box {
|
|
398
402
|
display: flex; flex-wrap: wrap; align-items: center; gap: var(--spacing-xs);
|
|
399
|
-
width: 100%;
|
|
403
|
+
width: 100%; min-width: 0;
|
|
400
404
|
min-height: var(--size-control-md);
|
|
401
405
|
padding: var(--spacing-xs);
|
|
402
406
|
border: 1px solid var(--color-input);
|
|
@@ -423,13 +427,14 @@ fieldset.form-body { border: 0; margin: 0; }
|
|
|
423
427
|
font: inherit; font-size: var(--type-size-body);
|
|
424
428
|
outline: none;
|
|
425
429
|
}
|
|
426
|
-
.chip-input-field::placeholder { color: var(--color-
|
|
430
|
+
.chip-input-field::placeholder { color: var(--color-muted-foreground); }
|
|
427
431
|
.chip-input-list {
|
|
428
432
|
position: absolute; inset-inline: 0; top: 100%; z-index: 40;
|
|
429
433
|
margin-block-start: var(--spacing-xs);
|
|
430
434
|
max-height: 240px; overflow-y: auto;
|
|
431
435
|
}
|
|
432
436
|
.chip-input-list .menu-item[aria-selected="true"] { background: var(--color-selected); color: var(--color-selected-foreground); }
|
|
437
|
+
.chip-input-list .menu-item[aria-selected="true"] .muted { color: inherit; }
|
|
433
438
|
.chip-input-list .menu-item .muted { margin-inline-start: auto; }
|
|
434
439
|
|
|
435
440
|
.chip {
|
|
@@ -445,6 +450,9 @@ fieldset.form-body { border: 0; margin: 0; }
|
|
|
445
450
|
font-size: var(--type-size-label);
|
|
446
451
|
line-height: 1;
|
|
447
452
|
}
|
|
453
|
+
/* A structured chip can scroll without widening its field or shrinking targets. */
|
|
454
|
+
.chip-input .chip:not(.filter-chip) { overflow-x: auto; }
|
|
455
|
+
.chip-input .chip:not(.filter-chip) > * { flex-shrink: 0; }
|
|
448
456
|
.chip:has(.is-editing) { background: var(--color-card); border-color: var(--color-primary); }
|
|
449
457
|
.chip[data-state="invalid"] { background: var(--color-danger-surface); border-color: var(--color-danger-border); color: var(--color-danger); }
|
|
450
458
|
/* The chip reads left to right like a call: what installs it, what it is,
|
|
@@ -476,8 +484,8 @@ fieldset.form-body { border: 0; margin: 0; }
|
|
|
476
484
|
.chip-option { color: var(--color-foreground); }
|
|
477
485
|
.chip-value:hover, .chip-option:hover { background: var(--color-accent-surface); }
|
|
478
486
|
.chip-option-name { color: var(--color-muted-foreground); }
|
|
479
|
-
.chip-option-mark { color: var(--color-
|
|
480
|
-
.chip-option[data-empty="true"] { color: var(--color-
|
|
487
|
+
.chip-option-mark { color: var(--color-muted-foreground); }
|
|
488
|
+
.chip-option[data-empty="true"] { color: var(--color-muted-foreground); }
|
|
481
489
|
.chip-value:last-child, .chip-option:last-child {
|
|
482
490
|
border-start-end-radius: var(--radius-xs);
|
|
483
491
|
border-end-end-radius: var(--radius-xs);
|
|
@@ -667,6 +675,9 @@ table.table { width: 100%; border-collapse: collapse; font-size: var(--type-size
|
|
|
667
675
|
.table .col-tight { width: 1%; white-space: nowrap; }
|
|
668
676
|
.th-sort { display: inline-flex; align-items: center; gap: var(--spacing-xs); cursor: pointer; background: none; border: 0; font: inherit; color: inherit; padding: 0; }
|
|
669
677
|
.table-toolbar { display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-md); border-bottom: 1px solid var(--color-border); }
|
|
678
|
+
/* In a toolbar a select is an accessory to the search, not a form field, so
|
|
679
|
+
it takes only what its longest option needs. */
|
|
680
|
+
.table-toolbar > .select { width: auto; flex: none; }
|
|
670
681
|
.table-footer { display: flex; align-items: center; justify-content: space-between; gap: var(--spacing-md); padding: var(--spacing-sm) var(--spacing-md); border-top: 1px solid var(--color-border); font-size: var(--type-size-label); color: var(--color-muted-foreground); }
|
|
671
682
|
|
|
672
683
|
/* ===========================================================================
|
|
@@ -682,12 +693,16 @@ table.table { width: 100%; border-collapse: collapse; font-size: var(--type-size
|
|
|
682
693
|
font-size: var(--type-size-body);
|
|
683
694
|
font-weight: var(--type-weight-medium);
|
|
684
695
|
color: var(--color-muted-foreground);
|
|
696
|
+
white-space: nowrap; /* a strip that scrolls sideways must not wrap "Last run" */
|
|
685
697
|
cursor: pointer;
|
|
686
698
|
}
|
|
687
699
|
.tabs button:hover { color: var(--color-foreground); }
|
|
688
700
|
.tabs button[aria-selected="true"] { color: var(--color-foreground); }
|
|
701
|
+
/* The underline stays inside the button, just above the strip's border, so a
|
|
702
|
+
strip that scrolls sideways has no extra pixel of block overflow to draw a
|
|
703
|
+
scrollbar for. */
|
|
689
704
|
.tabs button[aria-selected="true"]::after {
|
|
690
|
-
content: ""; position: absolute; inset-inline: 0; bottom:
|
|
705
|
+
content: ""; position: absolute; inset-inline: 0; bottom: 0; height: 2px;
|
|
691
706
|
background: var(--color-primary);
|
|
692
707
|
border-radius: var(--radius-full);
|
|
693
708
|
}
|
|
@@ -740,6 +755,7 @@ table.table { width: 100%; border-collapse: collapse; font-size: var(--type-size
|
|
|
740
755
|
.dialog-header { padding: var(--spacing-lg) var(--spacing-lg) 0; display: grid; gap: var(--spacing-2xs); }
|
|
741
756
|
.dialog-body { padding: var(--spacing-lg); display: grid; gap: var(--spacing-lg); }
|
|
742
757
|
.dialog-header + .dialog-body { padding-block-start: var(--spacing-md); }
|
|
758
|
+
.dialog-header + .dialog-footer { margin-block-start: var(--spacing-lg); }
|
|
743
759
|
.dialog-footer { padding: var(--spacing-md) var(--spacing-lg); border-top: 1px solid var(--color-border); display: flex; justify-content: flex-end; gap: var(--spacing-sm); background: var(--color-muted); border-end-start-radius: var(--radius-surface); border-end-end-radius: var(--radius-surface); }
|
|
744
760
|
|
|
745
761
|
/* ===========================================================================
|
|
@@ -989,7 +1005,7 @@ table.table { width: 100%; border-collapse: collapse; font-size: var(--type-size
|
|
|
989
1005
|
font-weight: var(--type-weight-medium);
|
|
990
1006
|
}
|
|
991
1007
|
|
|
992
|
-
.page { padding: var(--spacing-xl); display: grid; gap: var(--spacing-xl); align-content: start; max-width: var(--size-content-max); }
|
|
1008
|
+
.page { padding: var(--spacing-xl); display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--spacing-xl); align-content: start; min-width: 0; max-width: var(--size-content-max); }
|
|
993
1009
|
.page-header { display: grid; gap: var(--spacing-2xs); }
|
|
994
1010
|
|
|
995
1011
|
/* ===========================================================================
|
|
@@ -1042,7 +1058,7 @@ pre code { display: block; overflow-x: auto; padding: 0; background: none; borde
|
|
|
1042
1058
|
container scrolls for the corner marks it draws just outside its own frame.
|
|
1043
1059
|
Height goes on `.logview`; the scroller takes what is left. */
|
|
1044
1060
|
.logview > .log-scroll { flex: 1 1 auto; min-block-size: 0; overflow: auto; }
|
|
1045
|
-
.logview .log-time { color: var(--color-neutral-
|
|
1061
|
+
.logview .log-time { color: var(--color-neutral-500); margin-inline-end: var(--spacing-sm); }
|
|
1046
1062
|
.logview .log-warn { color: var(--color-warning-on-dark); }
|
|
1047
1063
|
.logview .log-error{ color: var(--color-danger-on-dark); }
|
|
1048
1064
|
.logview .log-info { color: var(--color-info-on-dark); }
|
|
@@ -1052,24 +1068,200 @@ pre code { display: block; overflow-x: auto; padding: 0; background: none; borde
|
|
|
1052
1068
|
.bars i { flex: 1; background: var(--color-border-strong); border-radius: var(--radius-xs); }
|
|
1053
1069
|
.bars i.on { background: var(--color-primary); }
|
|
1054
1070
|
|
|
1071
|
+
/* ===========================================================================
|
|
1072
|
+
Screen layout: list, detail and create screens (Kiso: list-detail, crud)
|
|
1073
|
+
=========================================================================== */
|
|
1074
|
+
/* The filter row on a list screen. Search first and growing, any extra
|
|
1075
|
+
filter after it, and "Clear filters" only once something is filtered. The
|
|
1076
|
+
way to add one more is in the page header, not here: narrowing a list and
|
|
1077
|
+
adding to it are opposite intentions. */
|
|
1078
|
+
.list-filters { display: flex; align-items: center; flex-wrap: wrap; gap: var(--spacing-sm); }
|
|
1079
|
+
.list-filters > .input-group { flex: 1 1 260px; min-width: 0; }
|
|
1080
|
+
/* The select is an accessory to the search, not a second field, so it takes
|
|
1081
|
+
only what its longest option needs. */
|
|
1082
|
+
.list-filters > .select { flex: none; width: auto; min-width: 11rem; }
|
|
1083
|
+
@media (max-width: 560px) {
|
|
1084
|
+
.list-filters > .input-group { flex-basis: 100%; }
|
|
1085
|
+
.list-filters > .select { width: 100%; }
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/* The detail screen's first row: the title on the left, the lifecycle row on
|
|
1089
|
+
the right. A title with a description is taller than the row, so the row
|
|
1090
|
+
sits at the top, and it wraps under the title when there is no room. */
|
|
1091
|
+
.between:has(> .lifecycle) { align-items: flex-start; flex-wrap: wrap; gap: var(--spacing-lg); }
|
|
1092
|
+
|
|
1093
|
+
/* The detail screen's header row: what it is, what you can do to it, and the
|
|
1094
|
+
one action you do not want to hit by accident. The two clusters sit a
|
|
1095
|
+
normal gap apart, and the destructive action gets a wider one so nothing is
|
|
1096
|
+
ever flush against it. */
|
|
1097
|
+
.lifecycle { display: flex; flex-wrap: wrap; align-items: center; gap: var(--spacing-md); }
|
|
1098
|
+
.lifecycle > .btn-danger-ghost { margin-inline-start: var(--spacing-sm); }
|
|
1099
|
+
|
|
1100
|
+
/* A cluster is one object made of several. The children give up their own
|
|
1101
|
+
border and corners to the frame, and a hairline between them is what says
|
|
1102
|
+
they are separate readings, or separate choices. */
|
|
1103
|
+
.cluster {
|
|
1104
|
+
display: inline-flex; align-items: stretch;
|
|
1105
|
+
height: var(--size-control-sm);
|
|
1106
|
+
border: 1px solid var(--color-border);
|
|
1107
|
+
border-radius: var(--radius-md);
|
|
1108
|
+
overflow: hidden;
|
|
1109
|
+
}
|
|
1110
|
+
/* Status is read, not pressed: no shadow, and each badge keeps the tone
|
|
1111
|
+
surface that carries its meaning. */
|
|
1112
|
+
.cluster-status > .badge {
|
|
1113
|
+
height: auto; border-radius: 0;
|
|
1114
|
+
border-block: 0; border-inline-end: 0;
|
|
1115
|
+
padding-inline: var(--spacing-md);
|
|
1116
|
+
font-size: var(--type-size-label);
|
|
1117
|
+
}
|
|
1118
|
+
/* The tone's own border would double up against the frame's line. */
|
|
1119
|
+
.cluster-status > .badge + .badge { border-inline-start-color: var(--color-border); }
|
|
1120
|
+
/* The verbs are pressed, so the group carries the surface and the shadow
|
|
1121
|
+
each button carried on its own. */
|
|
1122
|
+
.cluster-verbs { background: var(--color-card); box-shadow: var(--shadow-xs); }
|
|
1123
|
+
/* `.cluster` twice, so the flat corners beat the appearance rule that rounds
|
|
1124
|
+
every control and comes later in this file. */
|
|
1125
|
+
.cluster.cluster-verbs > .btn { border: 0; border-radius: 0; box-shadow: none; background: transparent; }
|
|
1126
|
+
.cluster-verbs > .btn + .btn { border-inline-start: 1px solid var(--color-border); }
|
|
1127
|
+
.cluster-verbs > .btn:hover:not(:disabled) { background: var(--color-accent-surface-hover); }
|
|
1128
|
+
/* A greyed verb is the row's normal state (Start is off while the thing
|
|
1129
|
+
runs), so it reads as unavailable without punching a hole in the group. */
|
|
1130
|
+
.cluster-verbs > .btn:disabled { background: var(--color-disabled-surface); color: var(--color-disabled); }
|
|
1131
|
+
/* The focus ring belongs on the group's edge, not on a square inside it. */
|
|
1132
|
+
.cluster-verbs > .btn:focus-visible { outline-offset: -2px; }
|
|
1133
|
+
/* Wrapped onto its own line, the destructive action has no gap left to set
|
|
1134
|
+
it apart, so the verbs take the row instead, and the row goes under the
|
|
1135
|
+
title rather than squeezing beside it. */
|
|
1136
|
+
@media (max-width: 720px) {
|
|
1137
|
+
.lifecycle { flex-basis: 100%; gap: var(--spacing-sm); }
|
|
1138
|
+
.cluster-verbs { width: 100%; }
|
|
1139
|
+
.cluster-verbs > .btn { flex: 1; }
|
|
1140
|
+
.lifecycle > .btn-danger-ghost { margin-inline-start: 0; }
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/* One card for everything a detail screen holds: its configuration, and
|
|
1144
|
+
whatever it prints. The card takes the height the header row leaves and
|
|
1145
|
+
each panel scrolls inside it, so switching tabs does not resize the page
|
|
1146
|
+
under the cursor. */
|
|
1147
|
+
.detail-tabs { min-width: 0; display: grid; grid-template-rows: minmax(0, 1fr); }
|
|
1148
|
+
.detail-tabs > [data-slot="tabs"] { display: grid; grid-template-rows: auto minmax(0, 1fr); min-height: 0; }
|
|
1149
|
+
/* The card does not clip, or its corner marks would go with the overflow.
|
|
1150
|
+
Its bottom corners pass down to the panel and the form instead, so a
|
|
1151
|
+
sticky footer and an edge-to-edge log follow the card's shape. */
|
|
1152
|
+
.detail-tabs > [data-slot="tabs"],
|
|
1153
|
+
.detail-tabs [role="tabpanel"],
|
|
1154
|
+
.detail-tabs [role="tabpanel"] > .form {
|
|
1155
|
+
border-end-start-radius: inherit; border-end-end-radius: inherit;
|
|
1156
|
+
}
|
|
1157
|
+
/* The strip scrolls sideways when the tabs do not fit. A scroller's automatic
|
|
1158
|
+
minimum size is zero, so the list keeps its own height explicitly. */
|
|
1159
|
+
.detail-tabs [role="tablist"] { max-width: 100%; overflow-x: auto; padding-inline: var(--spacing-xl); }
|
|
1160
|
+
.detail-tabs > [data-slot="tabs"] > [role="tablist"] { min-height: min-content; }
|
|
1161
|
+
.detail-tabs [role="tabpanel"] { min-width: 0; min-height: 0; overflow: auto; padding: var(--spacing-xl); }
|
|
1162
|
+
/* A panel with its own scroller, a log or a thread, takes the card out to
|
|
1163
|
+
its edges. */
|
|
1164
|
+
.detail-tabs [role="tabpanel"]:is(.detail-logs, .detail-pane) {
|
|
1165
|
+
display: grid; grid-template-rows: minmax(0, 1fr);
|
|
1166
|
+
overflow: hidden; padding: 0;
|
|
1167
|
+
}
|
|
1168
|
+
.detail-tabs .logview { min-height: 0; border: 0; border-radius: 0; --corner-mark: 0; }
|
|
1169
|
+
.detail-tabs .log-scroll { padding: var(--spacing-lg); }
|
|
1170
|
+
/* A display of our own beats the `hidden` a panel keeps after you leave it. */
|
|
1171
|
+
.detail-tabs [role="tabpanel"][hidden] { display: none; }
|
|
1172
|
+
/* A panel that holds a form gives its padding to the form's body, so a
|
|
1173
|
+
sticky FormActions spans the panel and rests on its bottom edge. */
|
|
1174
|
+
.detail-tabs [role="tabpanel"]:has(> .form) { padding: 0; }
|
|
1175
|
+
.detail-tabs .form-body { padding: var(--spacing-xl); }
|
|
1176
|
+
|
|
1177
|
+
/* A screen that fills the viewport: a detail card, a create card, or a list
|
|
1178
|
+
whose table is the page's last child. The page has to stop growing with
|
|
1179
|
+
its content, or the card and the table have no height to take. A screen
|
|
1180
|
+
opts out with `data-fill="false"` on the card or the `.table-wrap`, and
|
|
1181
|
+
the document scrolls as it did.
|
|
1182
|
+
Below 1024px the sidebar stacks above the content and the document
|
|
1183
|
+
scrolls again, so the cap is lifted with it. */
|
|
1184
|
+
@media (min-width: 1024px) {
|
|
1185
|
+
.app-shell:has(.page > :is(.detail-tabs, .form-page):not([data-fill="false"]), .page > .table-wrap:last-child:not([data-fill="false"])) { height: 100vh; }
|
|
1186
|
+
.app-shell:has(.page > :is(.detail-tabs, .form-page):not([data-fill="false"]), .page > .table-wrap:last-child:not([data-fill="false"])) > main {
|
|
1187
|
+
display: flex; flex-direction: column; min-height: 0;
|
|
1188
|
+
}
|
|
1189
|
+
.app-shell:has(.page > :is(.detail-tabs, .form-page):not([data-fill="false"]), .page > .table-wrap:last-child:not([data-fill="false"])) > main > * { flex: none; }
|
|
1190
|
+
.page:has(> :is(.detail-tabs, .form-page):not([data-fill="false"]), > .table-wrap:last-child:not([data-fill="false"])) { display: flex; flex-direction: column; min-height: 0; }
|
|
1191
|
+
.app-shell > main > .page:has(> :is(.detail-tabs, .form-page):not([data-fill="false"]), > .table-wrap:last-child:not([data-fill="false"])) { flex: 1; }
|
|
1192
|
+
/* `:where`, so the reset stays below the card's and the table's own flex. */
|
|
1193
|
+
.page:where(:has(> :is(.detail-tabs, .form-page):not([data-fill="false"]), > .table-wrap:last-child:not([data-fill="false"]))) > * { flex: none; }
|
|
1194
|
+
.page > :is(.detail-tabs, .form-page):not([data-fill="false"]) { flex: 1; min-height: 0; }
|
|
1195
|
+
/* The create card scrolls its form, so sticky actions rest on its edge. */
|
|
1196
|
+
.page > .form-page:not([data-fill="false"]) { display: grid; grid-template-rows: minmax(0, 1fr); }
|
|
1197
|
+
.page > .form-page:not([data-fill="false"]) > .form { min-height: 0; overflow: auto; }
|
|
1198
|
+
.page > .form-page[data-fill="false"] > .form > .form-actions[data-sticky="true"] { position: static; }
|
|
1199
|
+
/* The rows scroll inside the table, under its sticky header, and the count
|
|
1200
|
+
stays on the bottom edge. A page with more above the table than the
|
|
1201
|
+
viewport holds keeps a usable table and scrolls instead. */
|
|
1202
|
+
.page:has(> .table-wrap:last-child:not([data-fill="false"])) { overflow-y: auto; }
|
|
1203
|
+
.page > .table-wrap:last-child:not([data-fill="false"]) {
|
|
1204
|
+
flex: 1; min-height: calc(var(--size-control-lg) * 6);
|
|
1205
|
+
display: flex; flex-direction: column;
|
|
1206
|
+
}
|
|
1207
|
+
.page > .table-wrap:last-child:not([data-fill="false"]) > .table-scroll { flex: 1; min-height: 0; }
|
|
1208
|
+
.page > .table-wrap:last-child:not([data-fill="false"]) > .table-footer { flex: none; }
|
|
1209
|
+
}
|
|
1210
|
+
@media (max-width: 1023px) {
|
|
1211
|
+
.detail-tabs { grid-template-rows: auto; }
|
|
1212
|
+
/* The document scrolls, so the create form's actions end it in flow rather
|
|
1213
|
+
than float over a card whose marks would not follow them. */
|
|
1214
|
+
.form-page > .form > .form-actions[data-sticky="true"] { position: static; }
|
|
1215
|
+
.detail-tabs [role="tabpanel"] { overflow: visible; }
|
|
1216
|
+
.detail-tabs [role="tabpanel"]:is(.detail-logs, .detail-pane) { height: 60vh; }
|
|
1217
|
+
}
|
|
1218
|
+
@media (max-width: 720px) {
|
|
1219
|
+
.detail-tabs [role="tablist"] { padding-inline: var(--spacing-lg); }
|
|
1220
|
+
.detail-tabs [role="tabpanel"] { padding: var(--spacing-lg); }
|
|
1221
|
+
.detail-tabs .form-body { padding: var(--spacing-lg); }
|
|
1222
|
+
.detail-tabs [role="tabpanel"]:is(.detail-logs, .detail-pane) { padding: 0; }
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/* A create screen is the edit form in a card of its own. A form wider than a
|
|
1226
|
+
readable measure is only longer lines. The footer's corners follow the
|
|
1227
|
+
card's. From 1024px the card fills the viewport like a detail card, above. */
|
|
1228
|
+
.form-page { max-width: 720px; }
|
|
1229
|
+
.form-page > .form { border-radius: inherit; }
|
|
1230
|
+
|
|
1055
1231
|
/* ===========================================================================
|
|
1056
1232
|
Touch targets: the 44px rule applies to coarse pointers only.
|
|
1057
1233
|
This is the whole reason the desktop UI can be 36px.
|
|
1058
1234
|
=========================================================================== */
|
|
1059
1235
|
@media (pointer: coarse) {
|
|
1060
|
-
.btn, .input, .select, .nav-item, .menu-item {
|
|
1236
|
+
.btn, .input, .select, .nav-item, .menu-item, .tabs button {
|
|
1061
1237
|
min-height: var(--size-touch-min);
|
|
1062
1238
|
}
|
|
1063
|
-
.btn-icon { min-width: var(--size-touch-min); }
|
|
1239
|
+
.btn-icon, .tabs button { min-width: var(--size-touch-min); }
|
|
1240
|
+
.pagination { flex-wrap: wrap; }
|
|
1241
|
+
.pagination .btn { min-width: var(--size-touch-min); flex-shrink: 0; }
|
|
1242
|
+
/* Let the frame include the touch-sized buttons and both borders. */
|
|
1243
|
+
.cluster-verbs { height: auto; }
|
|
1064
1244
|
/* The chip grows with its segments, so the floor goes on the chip and the
|
|
1065
1245
|
segments stretch to it. */
|
|
1066
1246
|
.chip, .chip-input-field { min-height: var(--size-touch-min); }
|
|
1247
|
+
button.chip-value, button.chip-option,
|
|
1248
|
+
.chip-value.is-editing, .chip-option.is-editing {
|
|
1249
|
+
min-width: var(--size-touch-min); min-height: var(--size-touch-min);
|
|
1250
|
+
}
|
|
1067
1251
|
.chip-action { min-width: var(--size-touch-min); height: var(--size-touch-min); }
|
|
1068
1252
|
}
|
|
1069
1253
|
|
|
1254
|
+
/* Narrow viewports: the sidebar stacks above main as compact navigation
|
|
1255
|
+
instead of disappearing, since it may be the only way to reach a
|
|
1256
|
+
destination. It keeps its own scroller so a long list does not push the
|
|
1257
|
+
page down. */
|
|
1070
1258
|
@media (max-width: 1023px) {
|
|
1071
1259
|
.app-shell { grid-template-columns: 1fr; }
|
|
1072
|
-
.sidebar {
|
|
1260
|
+
.sidebar {
|
|
1261
|
+
position: static; height: auto; max-height: 40vh;
|
|
1262
|
+
border-inline-end: 0;
|
|
1263
|
+
border-block-end: 1px solid var(--color-sidebar-border);
|
|
1264
|
+
}
|
|
1073
1265
|
}
|
|
1074
1266
|
|
|
1075
1267
|
|
|
@@ -1190,7 +1382,10 @@ pre code { display: block; overflow-x: auto; padding: 0; background: none; borde
|
|
|
1190
1382
|
left calc(var(--corner-mark-tick) + var(--corner-mark-gap)) bottom 0,
|
|
1191
1383
|
right 0 bottom calc(var(--corner-mark-tick) + var(--corner-mark-gap)),
|
|
1192
1384
|
right calc(var(--corner-mark-tick) + var(--corner-mark-gap)) bottom 0;
|
|
1193
|
-
|
|
1385
|
+
/* Marks that are off (`--corner-mark: 0`) also collapse the inset to 0, so
|
|
1386
|
+
the pseudo-element stops reaching outside the frame and a scroll container
|
|
1387
|
+
around the panel does not scroll for marks nobody sees. */
|
|
1388
|
+
inset: calc(var(--corner-mark, 1) * var(--appearance-mark-inset, calc(-1 * (var(--corner-mark-tick) + var(--corner-mark-gap)))));
|
|
1194
1389
|
background-image: var(--appearance-mark-images,
|
|
1195
1390
|
linear-gradient(var(--color-corner-mark), var(--color-corner-mark)),
|
|
1196
1391
|
linear-gradient(var(--color-corner-mark), var(--color-corner-mark)),
|
|
@@ -1279,6 +1474,14 @@ pre code { display: block; overflow-x: auto; padding: 0; background: none; borde
|
|
|
1279
1474
|
.splitter { flex: none; width: 1px; background: var(--color-border); cursor: col-resize; position: relative; }
|
|
1280
1475
|
.splitter::after { content: ""; position: absolute; inset-block: 0; inset-inline: -3px; }
|
|
1281
1476
|
.splitter:hover, .splitter.dragging { background: var(--color-ring); }
|
|
1477
|
+
@media (pointer: coarse) {
|
|
1478
|
+
/* Reserve the hit area so it cannot cover controls at either pane's edge. */
|
|
1479
|
+
.splitter {
|
|
1480
|
+
margin-inline: calc((var(--size-touch-min) - 1px) / 2);
|
|
1481
|
+
touch-action: none;
|
|
1482
|
+
}
|
|
1483
|
+
.splitter::after { inset-inline: calc((1px - var(--size-touch-min)) / 2); }
|
|
1484
|
+
}
|
|
1282
1485
|
|
|
1283
1486
|
/* Legacy single-series SVG helpers. Interactive metrics use .framed-chart
|
|
1284
1487
|
below; these selectors remain compatible with existing consumers. */
|
|
@@ -1286,7 +1489,7 @@ pre code { display: block; overflow-x: auto; padding: 0; background: none; borde
|
|
|
1286
1489
|
.chart .grid { stroke: var(--color-border); stroke-width: 1; }
|
|
1287
1490
|
.chart .line { fill: none; stroke: currentColor; stroke-width: 1.5; }
|
|
1288
1491
|
.chart .area { fill: currentColor; fill-opacity: 0.12; }
|
|
1289
|
-
.chart-axis { display: flex; justify-content: space-between; font-size: var(--type-size-metadata); color: var(--color-
|
|
1492
|
+
.chart-axis { display: flex; justify-content: space-between; font-size: var(--type-size-metadata); color: var(--color-muted-foreground); margin-block-start: var(--spacing-sm); }
|
|
1290
1493
|
|
|
1291
1494
|
/* Sparkline — one metric series at cell size (issue #84). No axis, no grid,
|
|
1292
1495
|
no legend, no tooltip: the number beside the line carries the precision,
|
|
@@ -1320,7 +1523,8 @@ pre code { display: block; overflow-x: auto; padding: 0; background: none; borde
|
|
|
1320
1523
|
.meter, .bar-gauge { display: grid; gap: var(--spacing-sm); min-width: 0; }
|
|
1321
1524
|
.bar-gauge { gap: var(--spacing-md); }
|
|
1322
1525
|
.meter-label { display: flex; justify-content: space-between; gap: var(--spacing-md); font-size: var(--type-size-label); }
|
|
1323
|
-
.meter-label > :
|
|
1526
|
+
.meter-label > :first-child { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
1527
|
+
.meter-label > :last-child { max-width: 50%; overflow-wrap: anywhere; text-align: end; font-variant-numeric: tabular-nums; }
|
|
1324
1528
|
.meter-track { height: calc(var(--spacing-xs) + var(--spacing-2xs)); border-radius: var(--radius-xs); background-color: var(--color-secondary); overflow: hidden; }
|
|
1325
1529
|
.meter-track > span { display: block; height: 100%; background: var(--color-secondary-foreground); }
|
|
1326
1530
|
.disclosure { min-width: 0; }
|
|
@@ -1344,6 +1548,7 @@ pre code { display: block; overflow-x: auto; padding: 0; background: none; borde
|
|
|
1344
1548
|
.dashboard-panel, .dashboard-panel[data-span="12"] { grid-column: 1; }
|
|
1345
1549
|
}
|
|
1346
1550
|
@media (pointer: coarse) {
|
|
1551
|
+
.time-range-trigger { min-height: var(--size-touch-min); }
|
|
1347
1552
|
.disclosure > summary { min-height: var(--size-touch-min); display: list-item; align-content: center; }
|
|
1348
1553
|
}
|
|
1349
1554
|
|
|
@@ -1428,3 +1633,43 @@ button.step-row:focus-visible { outline: 2px solid var(--color-focus); outline-o
|
|
|
1428
1633
|
transition: none !important;
|
|
1429
1634
|
}
|
|
1430
1635
|
}
|
|
1636
|
+
|
|
1637
|
+
/* FilterInput: continuous typing with editable field/operator/value segments. */
|
|
1638
|
+
.filter-input { display: grid; gap: var(--spacing-sm); min-width: 0; }
|
|
1639
|
+
.filter-box { align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); }
|
|
1640
|
+
.filter-field-input { min-width: 12ch; flex: 1 1 18ch; font-family: var(--font-mono); }
|
|
1641
|
+
.filter-chip { padding-inline: 0 var(--spacing-xs); flex-wrap: wrap; }
|
|
1642
|
+
.filter-segment {
|
|
1643
|
+
display: inline-flex; align-items: center; flex-wrap: wrap; gap: var(--spacing-xs);
|
|
1644
|
+
max-width: 100%; min-width: 0; overflow-wrap: anywhere; white-space: normal;
|
|
1645
|
+
padding: var(--spacing-sm); border: 0; border-inline-end: 1px solid var(--color-border);
|
|
1646
|
+
min-height: var(--size-control-sm); background: transparent; color: var(--color-foreground);
|
|
1647
|
+
font-family: var(--font-mono); font-size: var(--type-size-metadata); cursor: pointer;
|
|
1648
|
+
}
|
|
1649
|
+
.filter-segment:hover { background: var(--color-accent-surface-hover); }
|
|
1650
|
+
.filter-operator { background: var(--color-card); color: var(--color-muted-foreground); }
|
|
1651
|
+
.filter-value { background: var(--color-card); color: var(--color-link); }
|
|
1652
|
+
.filter-list-value { padding: var(--spacing-2xs) var(--spacing-xs); background: var(--color-accent-surface); border-radius: var(--radius-sm); overflow-wrap: anywhere; }
|
|
1653
|
+
.filter-editor { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--spacing-xs); max-width: 100%; min-width: 0; }
|
|
1654
|
+
.filter-segment-input { border: 0; padding: var(--spacing-sm); background: var(--color-card); color: var(--color-foreground); font-family: var(--font-mono); font-size: var(--type-size-metadata); min-height: var(--size-control-sm); min-width: 4ch; max-width: 100%; }
|
|
1655
|
+
.filter-edit-actions, .filter-actions { display: flex; flex-wrap: wrap; gap: var(--spacing-xs); }
|
|
1656
|
+
.filter-join, .filter-boundary {
|
|
1657
|
+
border: 0; padding: var(--spacing-xs); background: transparent; color: var(--color-muted-foreground);
|
|
1658
|
+
font-family: var(--font-mono); font-size: var(--type-size-metadata); min-height: var(--size-control-sm); cursor: pointer;
|
|
1659
|
+
}
|
|
1660
|
+
.filter-join[data-join="OR"] { border-inline: 1px solid var(--color-border-strong); padding-inline: var(--spacing-sm); color: var(--color-link); }
|
|
1661
|
+
.filter-group { display: inline-flex; align-items: center; flex-wrap: wrap; gap: var(--spacing-sm); max-width: 100%; padding: var(--spacing-sm); border: 1px solid var(--color-border-strong); border-radius: var(--radius-md); background: var(--color-accent-surface); }
|
|
1662
|
+
.filter-group .filter-group { background: var(--color-card); }
|
|
1663
|
+
.filter-boundary { display: inline-flex; align-items: center; color: var(--color-link); }
|
|
1664
|
+
.filter-segment:focus-visible, .filter-segment-input:focus-visible, button.filter-boundary:focus-visible, .filter-join:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; }
|
|
1665
|
+
.filter-input button:disabled, .filter-segment-input:disabled { cursor: default; color: var(--color-disabled); }
|
|
1666
|
+
.filter-suggestions { position: absolute; z-index: 20; inset-inline: 0; top: calc(100% + var(--spacing-xs)); max-height: 20em; overflow: auto; box-shadow: var(--shadow-lg); }
|
|
1667
|
+
.filter-suggestions .menu-item { display: flex; justify-content: space-between; flex-wrap: wrap; gap: var(--spacing-sm); cursor: pointer; }
|
|
1668
|
+
.filter-suggestions .menu-item[aria-selected="true"] { background: var(--color-selected); color: var(--color-selected-foreground); }
|
|
1669
|
+
.filter-suggestions .menu-item[aria-selected="true"] .muted { color: inherit; }
|
|
1670
|
+
@media (pointer: coarse) {
|
|
1671
|
+
.filter-segment, .filter-join, button.filter-boundary, .filter-segment-input { min-height: var(--size-touch-min); }
|
|
1672
|
+
.filter-segment, .filter-join, button.filter-boundary { min-width: var(--size-touch-min); }
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
.filter-status { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
package/package.json
CHANGED
package/tokens/build/tokens.d.ts
CHANGED
|
@@ -125,7 +125,7 @@ export const semanticElevatedSurface: string;
|
|
|
125
125
|
export const semanticForeground: string;
|
|
126
126
|
/** Secondary text, labels. Text-eligible: passes WCAG AA 4.5:1 on background, surface and elevated-surface in both themes. */
|
|
127
127
|
export const semanticMutedForeground: string;
|
|
128
|
-
/**
|
|
128
|
+
/** Large supporting text and non-text graphics only (>=3:1 on their surface). Never small text. On dark this is dark.450, because dark.500 misses 3:1 on the dark surfaces. */
|
|
129
129
|
export const semanticSubtleForeground: string;
|
|
130
130
|
/** Dividers and input outlines. Non-text role. */
|
|
131
131
|
export const semanticBorder: string;
|
package/tokens/build/tokens.scss
CHANGED
|
@@ -90,7 +90,7 @@ $semantic-surface: #2d2b28; // Cards, panels, table rows. Surface role.
|
|
|
90
90
|
$semantic-elevated-surface: #3a3834; // Menus, popovers, dialogs. Surface role.
|
|
91
91
|
$semantic-foreground: #eeece9; // Primary text. Text-eligible: passes WCAG AA 4.5:1 on background, surface and elevated-surface in both themes.
|
|
92
92
|
$semantic-muted-foreground: #aba9a5; // Secondary text, labels. Text-eligible: passes WCAG AA 4.5:1 on background, surface and elevated-surface in both themes.
|
|
93
|
-
$semantic-subtle-foreground: #94928e; //
|
|
93
|
+
$semantic-subtle-foreground: #94928e; // Large supporting text and non-text graphics only (>=3:1 on their surface). Never small text. On dark this is dark.450, because dark.500 misses 3:1 on the dark surfaces.
|
|
94
94
|
$semantic-border: #494744; // Dividers and input outlines. Non-text role.
|
|
95
95
|
$semantic-primary: #684bb5; // Primary fill: primary buttons, solid badges, checked controls, the brand mark. Deep violet in both themes; on dark it is a dedicated fill and is not text-eligible, so ink roles (link, focus) take accent.base instead.
|
|
96
96
|
$semantic-accent: #ded5fb; // Emphasis and highlight. Text-eligible: passes WCAG AA 4.5:1 on background, surface and elevated-surface in both themes.
|