@simai-studio/view 0.0.0-stage → 1.0.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/LICENSE.md +183 -0
- package/README.md +75 -2
- package/dist/package-receipt.json +971 -0
- package/dist/set.json +141 -0
- package/dist/smart/rule.js +278 -0
- package/dist/smart/rule.js.gz +0 -0
- package/dist/smart/view/aliases.json +129 -0
- package/dist/smart/view/aliases.json.gz +0 -0
- package/dist/smart/view/component.json +1620 -0
- package/dist/smart/view/component.json.gz +0 -0
- package/dist/smart/view/css/view.css +208 -0
- package/dist/smart/view/css/view.css.gz +0 -0
- package/dist/smart/view/css/view.min.css +2 -0
- package/dist/smart/view/css/view.min.css.gz +0 -0
- package/dist/smart/view/examples.json +668 -0
- package/dist/smart/view/examples.json.gz +0 -0
- package/dist/smart/view/js/view.js +3938 -0
- package/dist/smart/view/js/view.js.gz +0 -0
- package/dist/smart/view/js/view.min.js +1 -0
- package/dist/smart/view/js/view.min.js.gz +0 -0
- package/dist/smart/view/lang/en.json +61 -0
- package/dist/smart/view/lang/en.json.gz +0 -0
- package/dist/smart/view/lang/ru.json +61 -0
- package/dist/smart/view/lang/ru.json.gz +0 -0
- package/dist/smart/view/probe.json +165 -0
- package/dist/smart/view/probe.json.gz +0 -0
- package/dist/smart/view/smart.manifest.json +1570 -0
- package/dist/smart/view/smart.manifest.json.gz +0 -0
- package/dist/smart/view/static/default/embedded.html +96 -0
- package/dist/smart/view/static/default/embedded.html.gz +0 -0
- package/dist/smart/view/static/default/open-record.html +247 -0
- package/dist/smart/view/static/default/open-record.html.gz +0 -0
- package/dist/smart/view/static/default/reference.html +247 -0
- package/dist/smart/view/static/default/reference.html.gz +0 -0
- package/dist/smart/view/static/sheets.json +282 -0
- package/dist/smart/view/static/sheets.json.gz +0 -0
- package/dist/smart/view-properties/aliases.json +174 -0
- package/dist/smart/view-properties/aliases.json.gz +0 -0
- package/dist/smart/view-properties/component.json +1898 -0
- package/dist/smart/view-properties/component.json.gz +0 -0
- package/dist/smart/view-properties/css/view-properties.css +1153 -0
- package/dist/smart/view-properties/css/view-properties.css.gz +0 -0
- package/dist/smart/view-properties/css/view-properties.min.css +2 -0
- package/dist/smart/view-properties/css/view-properties.min.css.gz +0 -0
- package/dist/smart/view-properties/examples.json +841 -0
- package/dist/smart/view-properties/examples.json.gz +0 -0
- package/dist/smart/view-properties/js/view-properties.js +10416 -0
- package/dist/smart/view-properties/js/view-properties.js.gz +0 -0
- package/dist/smart/view-properties/js/view-properties.min.js +1421 -0
- package/dist/smart/view-properties/js/view-properties.min.js.gz +0 -0
- package/dist/smart/view-properties/lang/en.json +96 -0
- package/dist/smart/view-properties/lang/en.json.gz +0 -0
- package/dist/smart/view-properties/lang/ru.json +96 -0
- package/dist/smart/view-properties/lang/ru.json.gz +0 -0
- package/dist/smart/view-properties/probe.json +188 -0
- package/dist/smart/view-properties/probe.json.gz +0 -0
- package/dist/smart/view-properties/skeleton.html +13 -0
- package/dist/smart/view-properties/skeleton.html.gz +0 -0
- package/dist/smart/view-properties/smart.manifest.json +1590 -0
- package/dist/smart/view-properties/smart.manifest.json.gz +0 -0
- package/dist/smart/view-properties/static/default/full.html +2 -0
- package/dist/smart/view-properties/static/default/full.html.gz +0 -0
- package/dist/smart/view-properties/static/default/panel-closed.html +4 -0
- package/dist/smart/view-properties/static/default/panel-closed.html.gz +0 -0
- package/dist/smart/view-properties/static/default/panel.html +4 -0
- package/dist/smart/view-properties/static/default/panel.html.gz +0 -0
- package/dist/smart/view-properties/static/default/reference.html +2 -0
- package/dist/smart/view-properties/static/default/reference.html.gz +0 -0
- package/dist/smart/view-properties/static/sheets.json +426 -0
- package/dist/smart/view-properties/static/sheets.json.gz +0 -0
- package/dist/smart/view-properties/template/default.css +989 -0
- package/dist/smart/view-properties/template/default.css.gz +0 -0
- package/dist/smart/view-properties/template/default.js +1660 -0
- package/dist/smart/view-properties/template/default.js.gz +0 -0
- package/dist/smart/view-table/aliases.json +448 -0
- package/dist/smart/view-table/aliases.json.gz +0 -0
- package/dist/smart/view-table/component.json +2738 -0
- package/dist/smart/view-table/component.json.gz +0 -0
- package/dist/smart/view-table/css/view-table.css +3658 -0
- package/dist/smart/view-table/css/view-table.css.gz +0 -0
- package/dist/smart/view-table/css/view-table.min.css +2 -0
- package/dist/smart/view-table/css/view-table.min.css.gz +0 -0
- package/dist/smart/view-table/examples.json +499 -0
- package/dist/smart/view-table/examples.json.gz +0 -0
- package/dist/smart/view-table/js/view-table.js +13879 -0
- package/dist/smart/view-table/js/view-table.js.gz +0 -0
- package/dist/smart/view-table/js/view-table.min.js +1440 -0
- package/dist/smart/view-table/js/view-table.min.js.gz +0 -0
- package/dist/smart/view-table/lang/en.json +132 -0
- package/dist/smart/view-table/lang/en.json.gz +0 -0
- package/dist/smart/view-table/lang/ru.json +132 -0
- package/dist/smart/view-table/lang/ru.json.gz +0 -0
- package/dist/smart/view-table/probe.json +275 -0
- package/dist/smart/view-table/probe.json.gz +0 -0
- package/dist/smart/view-table/smart.manifest.json +2524 -0
- package/dist/smart/view-table/smart.manifest.json.gz +0 -0
- package/dist/smart/view-table/static/default/current.html +141 -0
- package/dist/smart/view-table/static/default/current.html.gz +0 -0
- package/dist/smart/view-table/static/default/current.skeleton.html +141 -0
- package/dist/smart/view-table/static/default/current.skeleton.html.gz +0 -0
- package/dist/smart/view-table/static/default/empty.html +149 -0
- package/dist/smart/view-table/static/default/empty.html.gz +0 -0
- package/dist/smart/view-table/static/default/loading.html +180 -0
- package/dist/smart/view-table/static/default/loading.html.gz +0 -0
- package/dist/smart/view-table/static/default/many.html +180 -0
- package/dist/smart/view-table/static/default/many.html.gz +0 -0
- package/dist/smart/view-table/static/default/reference.html +136 -0
- package/dist/smart/view-table/static/default/reference.html.gz +0 -0
- package/dist/smart/view-table/static/default/reference.skeleton.html +136 -0
- package/dist/smart/view-table/static/default/reference.skeleton.html.gz +0 -0
- package/dist/smart/view-table/static/default/typed-cells.html +229 -0
- package/dist/smart/view-table/static/default/typed-cells.html.gz +0 -0
- package/dist/smart/view-table/static/default/typed-cells.skeleton.html +229 -0
- package/dist/smart/view-table/static/default/typed-cells.skeleton.html.gz +0 -0
- package/dist/smart/view-table/static/sheets.json +480 -0
- package/dist/smart/view-table/static/sheets.json.gz +0 -0
- package/dist/smart/view-table/studio.fixture.json +17 -0
- package/dist/smart/view-table/studio.fixture.json.gz +0 -0
- package/dist/smart/view-table/studio.scenario.json +30 -0
- package/dist/smart/view-table/studio.scenario.json.gz +0 -0
- package/dist/smart/view-table/template/default.css +1301 -0
- package/dist/smart/view-table/template/default.css.gz +0 -0
- package/dist/smart/view-table/template/default.js +1215 -0
- package/dist/smart/view-table/template/default.js.gz +0 -0
- package/dist/smart/view-table/template/link-cell-contract.js +48 -0
- package/dist/smart/view-table/template/link-cell-contract.js.gz +0 -0
- package/package.json +29 -4
|
@@ -0,0 +1,1301 @@
|
|
|
1
|
+
@layer sf.reset, sf.tokens, sf.base, sf.utilities, sf.components, sf.overrides, sf.states;
|
|
2
|
+
/* The table's published box (the :root block below, docs/docs.md) is
|
|
3
|
+
registered as lengths: a page that reads one from script
|
|
4
|
+
(getComputedStyle(document.documentElement).getPropertyValue(
|
|
5
|
+
'--sf-table-row-block-size-1/2') -- the name unescaped, as CSSOM spells it)
|
|
6
|
+
gets the length in px, not the token expression it is written in, and gets
|
|
7
|
+
it as soon as this sheet is linked, with no table on the page. */
|
|
8
|
+
@property --sf-table-row-block-size-1\/2 { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
9
|
+
@property --sf-table-row-block-size-1 { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
10
|
+
@property --sf-table-row-2-lines-block-size-1\/2 { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
11
|
+
@property --sf-table-row-2-lines-block-size-1 { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
12
|
+
@property --sf-table-head-block-size-1\/2 { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
13
|
+
@property --sf-table-head-block-size-1 { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
14
|
+
@property --sf-table-toolbar-block-size-1\/2 { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
15
|
+
@property --sf-table-toolbar-block-size-1 { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
16
|
+
@property --sf-table-toolbar-gap { syntax: "<length>"; inherits: true; initial-value: 0px; }
|
|
17
|
+
|
|
18
|
+
@layer sf.components {
|
|
19
|
+
/* The toolbar is one line while one line is enough, and two lines when it is
|
|
20
|
+
not — with the search, Create and the gear on the upper line and the filter
|
|
21
|
+
and its chips below. `wrap-reverse` is what does it: the search block is the
|
|
22
|
+
last child, so when it no longer fits beside the chips it wraps onto the
|
|
23
|
+
line above.
|
|
24
|
+
|
|
25
|
+
Focus has nothing to do with the arrangement any more. The search block used
|
|
26
|
+
to move up only while the field was focused and drop back below the chips
|
|
27
|
+
when it lost focus, so the panel rearranged itself under the person's hands.
|
|
28
|
+
The rule it replaced also re-rendered on focusin, and a render around a
|
|
29
|
+
focused field makes the browser lose the caret: text inserted without
|
|
30
|
+
keystrokes — layout switchers, dictation, on-screen keyboards — came out
|
|
31
|
+
reversed. Nothing here changes the DOM either. */
|
|
32
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar {
|
|
33
|
+
flex-wrap: wrap-reverse;
|
|
34
|
+
align-items: stretch;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* The search row keeps a usable width or goes to the line above. Without a
|
|
38
|
+
floor it simply shrank instead of wrapping, and the field ended up a stub
|
|
39
|
+
beside the chips. */
|
|
40
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar .sf-table-toolbar-end {
|
|
41
|
+
flex: 1 1 auto;
|
|
42
|
+
min-inline-size: var(--sf-g0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar .sf-search-input {
|
|
46
|
+
flex: 1 1 var(--sf-g0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* The field animates when it collapses and jumps when it expands, on
|
|
50
|
+
purpose. A width transition running under a focused field leaves the
|
|
51
|
+
caret uncommitted in Chrome: text inserted without keystrokes — layout
|
|
52
|
+
switchers, dictation, on-screen keyboards — then lands at position 0 and
|
|
53
|
+
comes out reversed. The transition property is read from the state being
|
|
54
|
+
entered, so naming it only on the unfocused state animates the collapse
|
|
55
|
+
and leaves the expansion instant. */
|
|
56
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar .sf-search-input {
|
|
57
|
+
transition-property: flex-grow, flex-shrink, flex-basis;
|
|
58
|
+
transition-duration: var(--sf-duration-fast);
|
|
59
|
+
transition-timing-function: var(--sf-ease-exit);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar:has(.sf-search-input :focus) .sf-search-input {
|
|
63
|
+
transition-property: none;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* In the list's toolbar the field takes what the three icons leave, from
|
|
67
|
+
nothing: a basis of g0 is wider than a phone with the icons beside it, and
|
|
68
|
+
the field overran the row to the left. */
|
|
69
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar.sf-table-toolbar--list {
|
|
70
|
+
flex-wrap: nowrap;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar--list > * {
|
|
74
|
+
min-inline-size: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar--list .sf-search-input {
|
|
78
|
+
flex: 1 1 0;
|
|
79
|
+
min-inline-size: 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* The native field asks for its size in characters; in the list it takes the
|
|
83
|
+
width it is given. */
|
|
84
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar--list .sf-search-input input {
|
|
85
|
+
inline-size: 100%;
|
|
86
|
+
min-inline-size: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Nothing in the toolbar clips a focus ring: a ring is drawn outside the
|
|
90
|
+
element's box, so an ancestor that hides its overflow cuts it off. */
|
|
91
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar,
|
|
92
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar-start,
|
|
93
|
+
:is(sf-view-table, sf-table) .sf-table-toolbar-end {
|
|
94
|
+
overflow: visible;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* The component table carries a bottom margin, which inside the scroll area is
|
|
98
|
+
an empty band between the last row and whatever follows — Show more, in the
|
|
99
|
+
view the consumer reported. The area ends where the rows end. */
|
|
100
|
+
:is(sf-view-table, sf-table) .sf-table-scroll > .table {
|
|
101
|
+
margin-block-end: 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
:is(sf-view-table, sf-table) > * .table > :not(caption) > * > * {
|
|
105
|
+
vertical-align: middle;
|
|
106
|
+
padding-block: var(--sf-space-1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* At step 1/2 a row is the ladder's: the 32px controls it holds and the step's
|
|
110
|
+
own vertical space around them -- a 45px row beside the admin menu's 32px
|
|
111
|
+
items, where step 1 keeps its 73. */
|
|
112
|
+
:is(sf-view-table, sf-table)[size="1/2"] > * .table > :not(caption) > * > * {
|
|
113
|
+
padding-block: var(--sf-ui-1\/2--space-y);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* A cell's text is its step's, not whatever the page's body is: the rows'
|
|
117
|
+
published heights (--sf-table-row-*-block-size-*, on the root above) are
|
|
118
|
+
then the rows drawn on every page. A two-line row at 1/2 was 61 px on a
|
|
119
|
+
page of 16/24 text and 53 on Larena's 14/20. */
|
|
120
|
+
:is(sf-view-table, sf-table) > * .table > :not(caption) > * > * {
|
|
121
|
+
font-size: var(--sf-text-size-1);
|
|
122
|
+
line-height: var(--sf-text-height-1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
:is(sf-view-table, sf-table)[size="1/2"] > * .table > :not(caption) > * > * {
|
|
126
|
+
font-size: var(--sf-text-size-1\/2);
|
|
127
|
+
line-height: var(--sf-text-height-1\/2);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* The windows' top-layer container (see getPortalContainer): the whole
|
|
131
|
+
viewport, invisible and click-through, so each window inside places itself
|
|
132
|
+
as it did at the end of the body. A popover is otherwise drawn as a small
|
|
133
|
+
centred box with a border, a fill and padding of the browser's own. */
|
|
134
|
+
.sf-table-portal:popover-open {
|
|
135
|
+
display: block;
|
|
136
|
+
position: fixed;
|
|
137
|
+
inset: 0;
|
|
138
|
+
inline-size: auto;
|
|
139
|
+
block-size: auto;
|
|
140
|
+
max-inline-size: none;
|
|
141
|
+
max-block-size: none;
|
|
142
|
+
margin: 0;
|
|
143
|
+
padding: 0;
|
|
144
|
+
border: 0;
|
|
145
|
+
background: transparent;
|
|
146
|
+
color: inherit;
|
|
147
|
+
overflow: visible;
|
|
148
|
+
pointer-events: none;
|
|
149
|
+
}
|
|
150
|
+
.sf-table-portal:popover-open > * {
|
|
151
|
+
pointer-events: auto;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* A window is as wide as what it holds, from the standard menu's floor to its
|
|
155
|
+
ceiling or the room the window has: every window was 24rem whatever it
|
|
156
|
+
held, and a list of two short field names stood with a third of the width
|
|
157
|
+
empty between the name and its checkbox (owner, 2026-10-14). */
|
|
158
|
+
.sf-table-context-menu {
|
|
159
|
+
min-width: min(var(--sf-context-menu--min-inline-size, var(--sf-e8)), var(--sf-table-context-menu--available-inline-size, 100vw));
|
|
160
|
+
max-width: min(var(--sf-table-context-menu--max-width, var(--sf-g2)), var(--sf-table-context-menu--available-inline-size, 100vw));
|
|
161
|
+
width: max-content;
|
|
162
|
+
/* A window as tall as its content puts its own buttons below the fold as
|
|
163
|
+
soon as a site has fifteen fields, and the page has to be scrolled to
|
|
164
|
+
reach them. The ceiling is the space left under the window's own top
|
|
165
|
+
edge, which only the component knows, so it arrives as a property. */
|
|
166
|
+
max-block-size: var(--sf-table-context-menu--max-block-size, none);
|
|
167
|
+
overflow: hidden;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* The chain a bounded window needs: every box between the ceiling and the
|
|
171
|
+
scrolling list must be allowed to shrink, or the list keeps its full
|
|
172
|
+
height and pushes the buttons out of the window instead. */
|
|
173
|
+
.sf-table-context-menu .sf-table-context-body {
|
|
174
|
+
min-block-size: 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* The search stood flush against the window edges while every other block in
|
|
178
|
+
the window is inset by the same step. */
|
|
179
|
+
/* The inline insets of every block in these windows come from the menu's own
|
|
180
|
+
published axis -- --sf-context-menu--item-inset-start-1 / -end-1 -- rather
|
|
181
|
+
than from a step written here. They had been a literal --sf-space-2, which
|
|
182
|
+
measured 20px against the axis's 17 on desktop and 16 against 15 on mobile:
|
|
183
|
+
straight inside the window, because the rows live inside these blocks, and
|
|
184
|
+
3px away from where every other menu in the Framework puts its content. A
|
|
185
|
+
second copy of the arithmetic is how the two drift apart; the fallback keeps
|
|
186
|
+
old runtimes rendering. */
|
|
187
|
+
.sf-table-context-menu .sf-table-column-search {
|
|
188
|
+
--sf-table-column-search--padding-inline-end: var(--sf-context-menu--inset-end);
|
|
189
|
+
--sf-table-column-search--padding-inline-start: var(--sf-context-menu--inset-start);
|
|
190
|
+
--sf-table-column-search--padding-top: var(--sf-context-menu--block-space);
|
|
191
|
+
padding-bottom: var(--sf-table-column-search--padding-bottom, 0);
|
|
192
|
+
padding-top: var(--sf-table-column-search--padding-top);
|
|
193
|
+
padding-inline-end: var(--sf-table-column-search--padding-inline-end);
|
|
194
|
+
padding-inline-start: var(--sf-table-column-search--padding-inline-start);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
:is(sf-view-table, sf-table) > .sf-table thead th[data-key="select"] {
|
|
198
|
+
--sf-table--select-column-width: var(--sf-text-height-1);
|
|
199
|
+
width: var(--sf-table--select-column-width);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
:is(sf-view-table, sf-table) > .sf-table thead th[data-key="settings"] {
|
|
203
|
+
--sf-table--settigs-column-width: calc(var(--sf-text-height-1) + var(--sf-ui-1--space-y-tightness-default));
|
|
204
|
+
width: var(--sf-table--settigs-column-width);
|
|
205
|
+
text-align: center;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* The table's cell spacing, declared rather than inherited. Measured at 2px,
|
|
209
|
+
which is the browser's own default for a separated-border table: nobody in
|
|
210
|
+
this Framework chose it, and it sits between every pair of cells and above
|
|
211
|
+
every row while the spacing ladder is used for everything else. The value is
|
|
212
|
+
kept exactly as it was shipping, so nothing moves, and it is now a token a
|
|
213
|
+
consumer can see and an owner can decide about.
|
|
214
|
+
|
|
215
|
+
The inset is published because a container that embeds a list has to put the
|
|
216
|
+
row text on the same inline axis as whatever stands beside it, and the only
|
|
217
|
+
honest way to do that is with the table's own number -- not a constant typed
|
|
218
|
+
into the embedder, which would drift the first time either value changed.
|
|
219
|
+
It is the spacing before the first cell plus that cell's own padding. */
|
|
220
|
+
/* On the root, not on sf-view-table: a container that embeds a list has to read the
|
|
221
|
+
inset to align to it, and a custom property declared on the table would only
|
|
222
|
+
be visible to the table and its descendants. The first version of this
|
|
223
|
+
declared it on sf-view-table, where the embedder above it read nothing and the
|
|
224
|
+
compensation silently came to zero. */
|
|
225
|
+
:root {
|
|
226
|
+
/* No space between cells. It was 2px, and on a highlighted, selected or
|
|
227
|
+
hovered row the gaps read as dark separators between every cell (owner,
|
|
228
|
+
2026-10-12). The row's own line below is the only rule a row needs; the
|
|
229
|
+
inset below is derived from this, so an embedded list stays on its axis. */
|
|
230
|
+
--sf-table--cell-spacing: 0px;
|
|
231
|
+
/* The narrowest a data column may be before the table scrolls instead of
|
|
232
|
+
squeezing further. Ten steps of the ladder, chosen by measurement rather
|
|
233
|
+
than by eye: at six the typed cells wrapped and a row grew from 57px to
|
|
234
|
+
81 where the typed-cells probe watches it, because a badge and its text
|
|
235
|
+
need more room than a date does. At ten both hold -- a column keeps a
|
|
236
|
+
date, a status badge or a heading on one line, and a row that is typed
|
|
237
|
+
is no taller than the same row plain (owner, 2026-10-09).
|
|
238
|
+
|
|
239
|
+
A host that knows better says so per column, and that wins. */
|
|
240
|
+
--sf-table--column-min-inline-size: var(--sf-f0);
|
|
241
|
+
/* The widest a column grows to fit its content on one line; past it the
|
|
242
|
+
value wraps. A host that knows better sets a width per column. */
|
|
243
|
+
--sf-table--column-max-inline-size: var(--sf-g0);
|
|
244
|
+
/* The cell's own leading padding, measured at 8px on the built bytes rather
|
|
245
|
+
than derived -- and 8px is --sf-a8, so it is named here rather than
|
|
246
|
+
written out again (owner, 2026-10-09). It is set by a nested rule in this sheet, and a nested rule's
|
|
247
|
+
selector is relative, so neither a grep nor `cell.matches()` can attribute
|
|
248
|
+
it -- the honest thing is to say the number was measured and let a check
|
|
249
|
+
hold it true. tests/browser/property-view asserts the row text of an
|
|
250
|
+
embedded list lands on the field-label axis, so if this padding changes that
|
|
251
|
+
probe goes red rather than an embedded list quietly drifting.
|
|
252
|
+
|
|
253
|
+
It was 1px before: that was the user agent's default, measured on a table
|
|
254
|
+
in a shadow tree where this sheet does not reach. The first version of this
|
|
255
|
+
inset was computed from that reading and put an embedded list 7px out once
|
|
256
|
+
the table was styled. */
|
|
257
|
+
--sf-table--row-text-inset: calc(var(--sf-table--cell-spacing) + var(--sf-a8));
|
|
258
|
+
|
|
259
|
+
/* The table's box, published so a page can hold the room of a list before
|
|
260
|
+
it draws -- min-block-size = toolbar + toolbar gap + head + rows x row
|
|
261
|
+
(docs.md) -- from the same tokens the rows below are drawn with, never a
|
|
262
|
+
number typed into the page. On the root for the reason above: the page
|
|
263
|
+
reads them outside the table. One value per step of the ladder (size
|
|
264
|
+
1/2 and 1); a row is its cells' padding at the step, its tallest
|
|
265
|
+
content -- the step's control (the row's box and menu) or its text,
|
|
266
|
+
one line or two -- and the line under it. */
|
|
267
|
+
--sf-table-row-block-size-1\/2: calc(2 * var(--sf-ui-1\/2--space-y) + max(var(--sf-ui-1\/2--control-height), var(--sf-text-height-1\/2)) + var(--sf-px));
|
|
268
|
+
--sf-table-row-block-size-1: calc(2 * var(--sf-space-1) + max(var(--sf-ui-1--control-height), var(--sf-text-height-1)) + var(--sf-px));
|
|
269
|
+
--sf-table-row-2-lines-block-size-1\/2: calc(2 * var(--sf-ui-1\/2--space-y) + max(var(--sf-ui-1\/2--control-height), 2 * var(--sf-text-height-1\/2)) + var(--sf-px));
|
|
270
|
+
--sf-table-row-2-lines-block-size-1: calc(2 * var(--sf-space-1) + max(var(--sf-ui-1--control-height), 2 * var(--sf-text-height-1)) + var(--sf-px));
|
|
271
|
+
/* The head row: the same padding, a line of the head's text, its line. */
|
|
272
|
+
--sf-table-head-block-size-1\/2: calc(2 * var(--sf-ui-1\/2--space-y) + var(--sf-text-height-1\/2) + var(--sf-px));
|
|
273
|
+
--sf-table-head-block-size-1: calc(2 * var(--sf-space-1) + var(--sf-text-height-1) + var(--sf-px));
|
|
274
|
+
/* The toolbar on one line (search, settings, export): the step's control,
|
|
275
|
+
and the gap between it and the head. */
|
|
276
|
+
--sf-table-toolbar-block-size-1\/2: var(--sf-ui-1\/2--control-height);
|
|
277
|
+
--sf-table-toolbar-block-size-1: var(--sf-ui-1--control-height);
|
|
278
|
+
--sf-table-toolbar-gap: var(--sf-space-1\/3);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/* The moment the table is measured for its content widths (see
|
|
282
|
+
measureContentWidths): laid out by content at its widest, with no column
|
|
283
|
+
width of its own. It is never painted like this. */
|
|
284
|
+
/* Once the columns carry their content widths the table is as wide as their
|
|
285
|
+
sum, and never narrower than its box. As wide as its content (w-max) it
|
|
286
|
+
took a long text's whole length and gave the surplus to that column, past
|
|
287
|
+
the cap it had been given. A fixed table is the greater of its width and
|
|
288
|
+
its columns' sum, so a width of 0 leaves the sum to decide. */
|
|
289
|
+
:is(sf-view-table, sf-table) table.sf-table-content-sized {
|
|
290
|
+
inline-size: 0;
|
|
291
|
+
/* Never narrower than its content (measureContentWidths): the column left
|
|
292
|
+
without a width takes the spare room, and where there is none it keeps
|
|
293
|
+
its content width rather than a letter's. */
|
|
294
|
+
min-inline-size: max(100%, var(--sf-table--content-inline-size, 0px));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* Content-sized but not measured yet -- the static markup a server prints
|
|
298
|
+
carries no measured size (standard 1.3.9 §5.4), nor do the columns' own
|
|
299
|
+
widths -- the table is laid out by its content, as wide as it is on one
|
|
300
|
+
line and never narrower than its box: what measureContentWidths will
|
|
301
|
+
give it. Fixed, without widths, it split a phone's 390 px evenly and
|
|
302
|
+
every cell wrapped (gate 24: 583 px against the drawn 271). */
|
|
303
|
+
:is(sf-view-table, sf-table) table.sf-table-content-sized:not([style*="--sf-table--content-inline-size"]) {
|
|
304
|
+
table-layout: auto;
|
|
305
|
+
inline-size: max-content;
|
|
306
|
+
min-inline-size: 100%;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
:is(sf-view-table, sf-table) table.sf-table-measuring {
|
|
310
|
+
table-layout: auto !important;
|
|
311
|
+
inline-size: max-content !important;
|
|
312
|
+
min-inline-size: 0 !important;
|
|
313
|
+
}
|
|
314
|
+
:is(sf-view-table, sf-table) table.sf-table-measuring col {
|
|
315
|
+
inline-size: auto !important;
|
|
316
|
+
width: auto !important;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
:is(sf-view-table, sf-table) .sf-table-cell-values {
|
|
320
|
+
flex-wrap: nowrap;
|
|
321
|
+
min-inline-size: 0;
|
|
322
|
+
overflow: hidden;
|
|
323
|
+
}
|
|
324
|
+
/* Values keep their width; one drawn by an element that is display:
|
|
325
|
+
contents (sf-tag) is its child's box. */
|
|
326
|
+
:is(sf-view-table, sf-table) .sf-table-cell-values > *,
|
|
327
|
+
:is(sf-view-table, sf-table) .sf-table-cell-values > * > * {
|
|
328
|
+
flex: none;
|
|
329
|
+
}
|
|
330
|
+
/* A folded value leaves the line but not the text: visually hidden, read. */
|
|
331
|
+
:is(sf-view-table, sf-table) .sf-table-cell-values .sf-table-cell-folded {
|
|
332
|
+
position: absolute;
|
|
333
|
+
inline-size: 1px;
|
|
334
|
+
block-size: 1px;
|
|
335
|
+
overflow: hidden;
|
|
336
|
+
clip-path: inset(50%);
|
|
337
|
+
white-space: nowrap;
|
|
338
|
+
}
|
|
339
|
+
:is(sf-view-table, sf-table) .sf-table-cell-more {
|
|
340
|
+
color: var(--sf-on-surface-variant);
|
|
341
|
+
font-size: var(--sf-text-size-1\/2);
|
|
342
|
+
white-space: nowrap;
|
|
343
|
+
}
|
|
344
|
+
:is(sf-view-table, sf-table) .sf-table-cell-more[hidden] {
|
|
345
|
+
display: none;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
:is(sf-view-table, sf-table) .sf-table-cell-clip {
|
|
349
|
+
display: block;
|
|
350
|
+
overflow: hidden;
|
|
351
|
+
text-overflow: ellipsis;
|
|
352
|
+
white-space: nowrap;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
:is(sf-view-table, sf-table) table {
|
|
356
|
+
border-spacing: var(--sf-table--cell-spacing);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
:is(sf-view-table, sf-table) th {
|
|
360
|
+
border-inline-start-width: var(--sf-px);
|
|
361
|
+
border-inline-start-style: solid;
|
|
362
|
+
border-inline-start-color: transparent;
|
|
363
|
+
border-inline-end-width: var(--sf-px);
|
|
364
|
+
border-inline-end-style: solid;
|
|
365
|
+
border-inline-end-color: transparent;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/* The head's own separator on hover: a neutral line, not `inherit`, which
|
|
369
|
+
took whatever colour the head text happened to carry and read as a blue bar
|
|
370
|
+
wherever that was the primary colour. */
|
|
371
|
+
:is(sf-view-table, sf-table) th.hover {
|
|
372
|
+
border-inline-end-color: var(--sf-outline);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
:is(sf-view-table, sf-table) .sf-table-sort-button {
|
|
376
|
+
padding: 0;
|
|
377
|
+
border: 0;
|
|
378
|
+
background: none;
|
|
379
|
+
color: inherit;
|
|
380
|
+
font: inherit;
|
|
381
|
+
text-align: start;
|
|
382
|
+
cursor: pointer;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
:is(sf-view-table, sf-table) .sf-table-sort-icon {
|
|
386
|
+
--sf-icon--font-size: var(--sf-icon-size-1);
|
|
387
|
+
opacity: 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
:is(sf-view-table, sf-table) .sf-table-sort-button[data-direction] .sf-table-sort-icon,
|
|
391
|
+
:is(sf-view-table, sf-table) th:hover .sf-table-sort-icon,
|
|
392
|
+
:is(sf-view-table, sf-table) .sf-table-sort-button:focus-visible .sf-table-sort-icon {
|
|
393
|
+
opacity: 1;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* The strip that takes the width: it sits astride the line between two
|
|
397
|
+
headers, is wide enough to hit with a mouse, and shows nothing of itself —
|
|
398
|
+
the line it is on is the affordance, and the cursor says what it does. */
|
|
399
|
+
:is(sf-view-table, sf-table) .sf-table-column-resizer {
|
|
400
|
+
position: absolute;
|
|
401
|
+
inset-block: 0;
|
|
402
|
+
inset-inline-end: calc(var(--sf-b0) / -2);
|
|
403
|
+
inline-size: var(--sf-b0);
|
|
404
|
+
padding: 0;
|
|
405
|
+
border: 0;
|
|
406
|
+
background: none;
|
|
407
|
+
color: inherit;
|
|
408
|
+
cursor: col-resize;
|
|
409
|
+
touch-action: none;
|
|
410
|
+
z-index: 2;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* The keyboard reaches it, so it has to show where it is: the shared ring,
|
|
414
|
+
turned inwards, because the strip is flush with the headers on both sides
|
|
415
|
+
and a ring drawn outside it would sit on their text. */
|
|
416
|
+
:is(sf-view-table, sf-table) .sf-table-column-resizer:focus-visible {
|
|
417
|
+
outline: var(--sf-focus--width) var(--sf-focus--style) var(--sf-focus--color);
|
|
418
|
+
outline-offset: calc(var(--sf-focus--width) * -1);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
:is(sf-view-table, sf-table) th.is-resizing {
|
|
422
|
+
user-select: none;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
:is(sf-view-table, sf-table) th.is-resizing-forward {
|
|
426
|
+
border-inline-end-color: var(--sf-primary);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
:is(sf-view-table, sf-table) th.is-resizing-backward {
|
|
430
|
+
border-inline-start-color: var(--sf-primary);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* A heading being dragged (SF.Sortable.attach): a sortable ghost follows
|
|
434
|
+
the pointer and the heading text is not selected on the way. */
|
|
435
|
+
html[data-sf-sortable-active] :is(sf-view-table, sf-table) th {
|
|
436
|
+
user-select: none;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.sf-table-column-resizing,
|
|
440
|
+
.sf-table-column-resizing * {
|
|
441
|
+
cursor: col-resize !important;
|
|
442
|
+
user-select: none !important;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/* The actions column's width is its <col>'s: as wide as the buttons it holds
|
|
446
|
+
(sf-view-table actionsColumnWidth). A width on the head cell won in a fixed
|
|
447
|
+
layout and held it at f0 -- 160px around one eye icon. */
|
|
448
|
+
|
|
449
|
+
/* The head paints its own surface, and the colour has a role behind it.
|
|
450
|
+
Until now it sat on `thead` alone, under a custom property nothing defines,
|
|
451
|
+
so the head was transparent — invisible while the table rested on a surface
|
|
452
|
+
of the same colour, and plainly wrong once the head became sticky: a sticky
|
|
453
|
+
`th` leaves the box `thead` paints, and the rows scrolled through the
|
|
454
|
+
labels. The fallback is `--sf-surface-0`, the surface `body` itself paints,
|
|
455
|
+
because a table that is not inside a raised box rests on that one; a host
|
|
456
|
+
that puts the table on another surface names it through the property. */
|
|
457
|
+
:is(sf-view-table, sf-table) > .sf-table thead,
|
|
458
|
+
:is(sf-view-table, sf-table) > .sf-table thead th {
|
|
459
|
+
background-color: var(--sf-table-head--background-color, var(--sf-surface-0));
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
:is(sf-view-table, sf-table) > * .sf-table-avatar {
|
|
463
|
+
max-width: var(--sf-table-avatar--max-width);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
:is(sf-view-table, sf-table) .sf-table-link-cell {
|
|
467
|
+
color: inherit;
|
|
468
|
+
display: inline-flex;
|
|
469
|
+
max-width: 100%;
|
|
470
|
+
min-width: 0;
|
|
471
|
+
text-decoration: none;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* One link in a table (owner, 2026-10-14): the link colour, no underline at
|
|
475
|
+
rest, the underline on hover and focus -- the title, a link cell and a
|
|
476
|
+
related record alike. Three styles side by side were visual noise. */
|
|
477
|
+
:is(sf-view-table, sf-table) a.sf-table-link-cell .sf-table-link-cell-text,
|
|
478
|
+
:is(sf-view-table, sf-table) .sf-table-title-link,
|
|
479
|
+
:is(sf-view-table, sf-table) .sf-reference-link :where(.sf-reference-link__link, .sf-reference-link-left > a) {
|
|
480
|
+
color: var(--sf-link);
|
|
481
|
+
text-decoration: none;
|
|
482
|
+
text-underline-offset: var(--sf-a3);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
:is(sf-view-table, sf-table) a.sf-table-link-cell:is(:hover, :focus-visible) .sf-table-link-cell-text,
|
|
486
|
+
:is(sf-view-table, sf-table) .sf-table-title-link:is(:hover, :focus-visible),
|
|
487
|
+
:is(sf-view-table, sf-table) .sf-reference-link :where(.sf-reference-link__link, .sf-reference-link-left > a):is(:hover, :focus-visible) {
|
|
488
|
+
text-decoration: underline;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
:is(sf-view-table, sf-table) .sf-table-link-cell-text,
|
|
492
|
+
:is(sf-view-table, sf-table) .sf-table-link-cell-subtext {
|
|
493
|
+
overflow: hidden;
|
|
494
|
+
text-overflow: ellipsis;
|
|
495
|
+
white-space: nowrap;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
:is(sf-view-table, sf-table) .sf-table-link-cell-subtext {
|
|
499
|
+
color: var(--sf-on-surface-variant);
|
|
500
|
+
}
|
|
501
|
+
.sf-table-context-menu .sf-table-context-top {
|
|
502
|
+
--sf-table-context-top--padding-inline-end: var(--sf-context-menu--inset-end);
|
|
503
|
+
--sf-table-context-top--padding-inline-start: var(--sf-context-menu--inset-start);
|
|
504
|
+
--sf-table-context-top--padding-top: var(--sf-context-menu--block-space);
|
|
505
|
+
padding-bottom: var(--sf-table-context-top--padding-bottom, 0);
|
|
506
|
+
padding-top: var(--sf-table-context-top--padding-top);
|
|
507
|
+
padding-inline-end: var(--sf-table-context-top--padding-inline-end);
|
|
508
|
+
padding-inline-start: var(--sf-table-context-top--padding-inline-start);
|
|
509
|
+
}
|
|
510
|
+
.sf-table-context-menu .sf-table-context-main {
|
|
511
|
+
--sf-table-context-main--padding-inline-end: var(--sf-context-menu--inset-end);
|
|
512
|
+
--sf-table-context-main--padding-inline-start: var(--sf-context-menu--inset-start);
|
|
513
|
+
padding-bottom: var(--sf-table-context-main--padding-bottom, 0);
|
|
514
|
+
padding-top: var(--sf-table-context-main--padding-top,0);
|
|
515
|
+
padding-inline-end: var(--sf-table-context-main--padding-inline-end);
|
|
516
|
+
padding-inline-start: var(--sf-table-context-main--padding-inline-start);
|
|
517
|
+
}
|
|
518
|
+
/* The window's buttons are the standard menu's footer
|
|
519
|
+
(.sf-context-menu-footer): its insets, separator and shared width. */
|
|
520
|
+
.sf-table-context-menu .sf-context-menu-columns {
|
|
521
|
+
--sf-context-menu-columns--padding-inline-end: var(--sf-context-menu--inset-end);
|
|
522
|
+
--sf-context-menu-columns--padding-inline-start: var(--sf-context-menu--inset-start);
|
|
523
|
+
--sf-context-menu-columns--padding-top: var(--sf-context-menu--block-space);
|
|
524
|
+
--sf-context-menu-columns--padding-bottom: var(--sf-context-menu--block-space);
|
|
525
|
+
padding-bottom: var(--sf-context-menu-columns--padding-bottom, 0);
|
|
526
|
+
padding-top: var(--sf-context-menu-columns--padding-top);
|
|
527
|
+
padding-inline-end: var(--sf-context-menu-columns--padding-inline-end);
|
|
528
|
+
padding-inline-start: var(--sf-context-menu-columns--padding-inline-start);
|
|
529
|
+
}
|
|
530
|
+
/* A table's windows are standard menus at the table's size (owner,
|
|
531
|
+
2026-10-14): their blocks take the menu's insets and block space, so a
|
|
532
|
+
window at 1/2 reads at the step of the list it belongs to. */
|
|
533
|
+
/* Skeleton bars at the dense step are about the height of the row's
|
|
534
|
+
lowercase letters, not of the whole text line: at a text line's height a
|
|
535
|
+
bar read heavier than the text it stands for (owner, 2026-10-13). */
|
|
536
|
+
:is(sf-view-table, sf-table)[size="1/2"] .sf-table-skeleton .sf-skeleton-text {
|
|
537
|
+
--sf-skeleton-text--height: var(--sf-b0);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/* A loading row is as tall as a loaded one: the bar stands in a line of the
|
|
541
|
+
row's own text, and the controls' columns hold a block of that line. */
|
|
542
|
+
:is(sf-view-table, sf-table) .sf-table-skeleton .sf-skeleton-text {
|
|
543
|
+
--sf-skeleton-text--line: 1lh;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
:is(sf-view-table, sf-table) .sf-table-skeleton-control {
|
|
547
|
+
block-size: 1lh;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/* A window's own buttons keep their words on one line: a longer label
|
|
551
|
+
(«Применить фильтр» once) was two pixels wider than half the row and broke in two, making the
|
|
552
|
+
buttons twice as tall as the controls above them. */
|
|
553
|
+
.sf-table-context-menu .sf-context-menu-footer .sf-button-text-container {
|
|
554
|
+
white-space: nowrap;
|
|
555
|
+
}
|
|
556
|
+
.sf-table-context-menu .sf-settings-context-menu-item [data-keep]:not(.sf-button-pinned) {
|
|
557
|
+
opacity: 0;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.sf-table-context-menu .sf-settings-context-menu-item.is-dragging {
|
|
561
|
+
opacity: 0.8;
|
|
562
|
+
/* A dragged item is lifted to step 3: surface and shadow, or it is flat
|
|
563
|
+
in the dark theme. */
|
|
564
|
+
background-color: var(--sf-elevation-3--surface);
|
|
565
|
+
--sf-on-surface-variant: var(--sf-on-elevation-3--variant);
|
|
566
|
+
box-shadow: var(--sf-elevation-3--shadow);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/* Where a dragged row will land: the Framework's silhouette (sf-sortable
|
|
570
|
+
preview="silhouette"), an outline of its size with a faint fill, the same
|
|
571
|
+
in every list the admin reorders (owner, 2026-10-13). A placeholder at a
|
|
572
|
+
third of the row's opacity read as a ghost of the row, not as a place. */
|
|
573
|
+
.sf-table-context-menu .sf-settings-context-menu-item.drag-placeholder,
|
|
574
|
+
:is(sf-view-table, sf-table) .drag-placeholder {
|
|
575
|
+
box-sizing: border-box;
|
|
576
|
+
border: var(--sf-px) dashed var(--sf-outline-primary);
|
|
577
|
+
border-radius: var(--sf-radius--ui);
|
|
578
|
+
background-color: var(--sf-primary-transparent-select);
|
|
579
|
+
pointer-events: none;
|
|
580
|
+
}
|
|
581
|
+
.sf-table-context-menu .sf-settings-context-menu-item.drag-placeholder > *,
|
|
582
|
+
:is(sf-view-table, sf-table) .drag-placeholder > * {
|
|
583
|
+
visibility: hidden;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/* A tree row (data view port 1.8.0): the first data cell is indented by the
|
|
587
|
+
row's level, one icon step per level, and leads with the button that
|
|
588
|
+
opens or closes the node -- or with its width, so leaves line up with the
|
|
589
|
+
nodes beside them. */
|
|
590
|
+
:is(sf-view-table, sf-table) .sf-table-tree-cell {
|
|
591
|
+
display: flex;
|
|
592
|
+
align-items: center;
|
|
593
|
+
gap: var(--sf-a4);
|
|
594
|
+
min-inline-size: 0;
|
|
595
|
+
padding-inline-start: calc(var(--sf-table-tree--level, 0) * var(--sf-icon-size-2));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
:is(sf-view-table, sf-table) .sf-table-tree-toggle {
|
|
599
|
+
flex: none;
|
|
600
|
+
display: inline-flex;
|
|
601
|
+
align-items: center;
|
|
602
|
+
justify-content: center;
|
|
603
|
+
inline-size: var(--sf-icon-size-2);
|
|
604
|
+
block-size: var(--sf-icon-size-2);
|
|
605
|
+
padding: 0;
|
|
606
|
+
border: 0;
|
|
607
|
+
border-radius: var(--sf-radius--ui);
|
|
608
|
+
background: none;
|
|
609
|
+
color: var(--sf-on-surface-variant);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
:is(sf-view-table, sf-table) button.sf-table-tree-toggle {
|
|
613
|
+
cursor: pointer;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
:is(sf-view-table, sf-table) button.sf-table-tree-toggle:focus-visible {
|
|
617
|
+
outline: var(--sf-focus--width) var(--sf-focus--style) var(--sf-focus--color);
|
|
618
|
+
outline-offset: var(--sf-focus--offset, 0);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
:is(sf-view-table, sf-table) .sf-table-tree-toggle .sf-icon {
|
|
622
|
+
--sf-icon--color: currentColor;
|
|
623
|
+
transition: transform var(--sf-duration-fast);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
:is(sf-view-table, sf-table) .sf-table-tree-toggle:dir(rtl) .sf-icon {
|
|
627
|
+
transform: rotate(180deg);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
:is(sf-view-table, sf-table) .sf-table-tree-toggle[data-expanded] .sf-icon {
|
|
631
|
+
transform: rotate(90deg);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/* Named for assistive technology, not drawn: the actions column's heading. */
|
|
635
|
+
:is(sf-view-table, sf-table) .sf-table-visually-hidden {
|
|
636
|
+
position: absolute;
|
|
637
|
+
inline-size: var(--sf-px);
|
|
638
|
+
block-size: var(--sf-px);
|
|
639
|
+
margin: calc(var(--sf-px) * -1);
|
|
640
|
+
padding: 0;
|
|
641
|
+
overflow: hidden;
|
|
642
|
+
clip-path: inset(50%);
|
|
643
|
+
white-space: nowrap;
|
|
644
|
+
border: 0;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/* A cell's plain text, three lines at most. */
|
|
648
|
+
:is(sf-view-table, sf-table) .sf-table-cell-text {
|
|
649
|
+
display: -webkit-box;
|
|
650
|
+
-webkit-box-orient: vertical;
|
|
651
|
+
-webkit-line-clamp: var(--sf-table-cell--line-clamp, 3);
|
|
652
|
+
line-clamp: var(--sf-table-cell--line-clamp, 3);
|
|
653
|
+
overflow: hidden;
|
|
654
|
+
overflow-wrap: anywhere;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/* The node's icon: one icon step, on the line, in the variant role -- it
|
|
658
|
+
says what the node is, the title says which. */
|
|
659
|
+
:is(sf-view-table, sf-table) .sf-table-tree-icon {
|
|
660
|
+
--sf-icon--color: var(--sf-on-surface-variant);
|
|
661
|
+
--sf-icon--font-size: var(--sf-icon-size-2);
|
|
662
|
+
--sf-icon--width: var(--sf-icon-size-2);
|
|
663
|
+
--sf-icon--height: var(--sf-icon-size-2);
|
|
664
|
+
flex: none;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
:is(sf-view-table, sf-table) .sf-table-tree-path {
|
|
668
|
+
overflow: hidden;
|
|
669
|
+
text-overflow: ellipsis;
|
|
670
|
+
white-space: nowrap;
|
|
671
|
+
color: var(--sf-on-surface-variant);
|
|
672
|
+
font-size: var(--sf-text-size-1\/3);
|
|
673
|
+
line-height: var(--sf-text-height-1\/3);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/* ─── the compact list (layout list, or auto below list-below) ───────────
|
|
677
|
+
A record is a block: its box, its title and a few fields under it, its row
|
|
678
|
+
menu. It carries the row's states with the row's colours, so selecting and
|
|
679
|
+
hovering read the same in both. */
|
|
680
|
+
:is(sf-view-table, sf-table) .sf-table-list {
|
|
681
|
+
display: flex;
|
|
682
|
+
flex-direction: column;
|
|
683
|
+
margin: 0;
|
|
684
|
+
padding: 0;
|
|
685
|
+
list-style: none;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
:is(sf-view-table, sf-table) .sf-table-list-item {
|
|
689
|
+
display: grid;
|
|
690
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
691
|
+
align-items: start;
|
|
692
|
+
gap: var(--sf-a8);
|
|
693
|
+
padding: var(--sf-a8);
|
|
694
|
+
border-block-end: var(--sf-px) solid var(--sf-outline-variant);
|
|
695
|
+
background-color: var(--sf-table-row--background-color, var(--sf-surface-0));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
:is(sf-view-table, sf-table) .sf-table-list-item:hover {
|
|
699
|
+
background-image: linear-gradient(var(--sf-surface-container-hover), var(--sf-surface-container-hover));
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
:is(sf-view-table, sf-table) .sf-table-list-item[data-selected] {
|
|
703
|
+
background-image: linear-gradient(var(--sf-primary-container), var(--sf-primary-container));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
:is(sf-view-table, sf-table) .sf-table-list-item[data-selected]:hover {
|
|
707
|
+
background-image: linear-gradient(var(--sf-primary-container-hover), var(--sf-primary-container-hover));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
:is(sf-view-table, sf-table) .sf-table-list-item:focus-visible {
|
|
711
|
+
outline: var(--sf-focus--width) var(--sf-focus--style) var(--sf-focus--color);
|
|
712
|
+
outline-offset: calc(var(--sf-focus--width) * -1);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
:is(sf-view-table, sf-table) .sf-table-list-main {
|
|
716
|
+
display: flex;
|
|
717
|
+
flex-direction: column;
|
|
718
|
+
gap: var(--sf-a4);
|
|
719
|
+
min-inline-size: 0;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
:is(sf-view-table, sf-table) .sf-table-list-title {
|
|
723
|
+
min-inline-size: 0;
|
|
724
|
+
overflow-wrap: anywhere;
|
|
725
|
+
font-weight: 500;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
:is(sf-view-table, sf-table) .sf-table-list-fields {
|
|
729
|
+
display: flex;
|
|
730
|
+
flex-wrap: wrap;
|
|
731
|
+
gap: var(--sf-a4) var(--sf-space-1\/2);
|
|
732
|
+
min-inline-size: 0;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
:is(sf-view-table, sf-table) .sf-table-list-field {
|
|
736
|
+
display: inline-flex;
|
|
737
|
+
flex-wrap: wrap;
|
|
738
|
+
align-items: center;
|
|
739
|
+
gap: var(--sf-a4);
|
|
740
|
+
min-inline-size: 0;
|
|
741
|
+
max-inline-size: 100%;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
:is(sf-view-table, sf-table) .sf-table-list-label {
|
|
745
|
+
color: var(--sf-on-surface-variant);
|
|
746
|
+
font-size: var(--sf-text-size-1\/2);
|
|
747
|
+
line-height: var(--sf-text-height-1\/2);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
:is(sf-view-table, sf-table) .sf-table-list-value {
|
|
751
|
+
min-inline-size: 0;
|
|
752
|
+
overflow-wrap: anywhere;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/* Several values -- a page's badge per language -- wrap whole rather than
|
|
756
|
+
each shrinking to an ellipsis. */
|
|
757
|
+
:is(sf-view-table, sf-table) .sf-table-list-value > .flex-wrap > * {
|
|
758
|
+
flex-shrink: 0;
|
|
759
|
+
max-inline-size: 100%;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
:is(sf-view-table, sf-table) .sf-table-list-more {
|
|
763
|
+
padding: var(--sf-a4) var(--sf-a8);
|
|
764
|
+
padding-inline-start: calc(var(--sf-a8) + var(--sf-table-tree--level, 0) * var(--sf-icon-size-2));
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/* The filter button carries how many conditions are on. */
|
|
768
|
+
:is(sf-view-table, sf-table) .sf-table-list-filter {
|
|
769
|
+
position: relative;
|
|
770
|
+
display: inline-flex;
|
|
771
|
+
flex: none;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
:is(sf-view-table, sf-table) .sf-table-list-filter-count {
|
|
775
|
+
position: absolute;
|
|
776
|
+
inset-block-start: calc(var(--sf-a4) * -1);
|
|
777
|
+
inset-inline-end: calc(var(--sf-a4) * -1);
|
|
778
|
+
min-inline-size: var(--sf-text-height-1\/2);
|
|
779
|
+
padding-inline: var(--sf-a2);
|
|
780
|
+
border-radius: var(--sf-radius-circle);
|
|
781
|
+
background-color: var(--sf-error);
|
|
782
|
+
color: var(--sf-on-error);
|
|
783
|
+
font-size: var(--sf-text-size-1\/2);
|
|
784
|
+
line-height: var(--sf-text-height-1\/2);
|
|
785
|
+
text-align: center;
|
|
786
|
+
pointer-events: none;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/* Active filters: one row of chips that scrolls sideways, so the list starts
|
|
790
|
+
where it starts however many there are. */
|
|
791
|
+
:is(sf-view-table, sf-table) .sf-table-list-tags {
|
|
792
|
+
flex-wrap: nowrap;
|
|
793
|
+
overflow-x: auto;
|
|
794
|
+
scrollbar-width: none;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
:is(sf-view-table, sf-table) .sf-table-list-tags > * {
|
|
798
|
+
flex: none;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/* The filter in the list opens as a sheet over the whole screen: a menu hung
|
|
802
|
+
from a 32px button on a phone leaves no room for its fields. The menu
|
|
803
|
+
lives in the table's portal, outside sf-view-table, and places itself with
|
|
804
|
+
inline styles from the button; the sheet's rule overrides them. */
|
|
805
|
+
.sf-table-context-layer[data-sf-sheet] .sf-table-context-menu {
|
|
806
|
+
position: fixed !important;
|
|
807
|
+
inset: 0 !important;
|
|
808
|
+
transform: none !important;
|
|
809
|
+
translate: none !important;
|
|
810
|
+
inline-size: 100% !important;
|
|
811
|
+
max-inline-size: none !important;
|
|
812
|
+
block-size: 100% !important;
|
|
813
|
+
max-block-size: none !important;
|
|
814
|
+
margin: 0 !important;
|
|
815
|
+
border-radius: 0 !important;
|
|
816
|
+
overflow-y: auto;
|
|
817
|
+
/* The sheet is the screen, so it paints the screen; the panel inside paints
|
|
818
|
+
only as far as its fields reach. Room is left at the top for the way out. */
|
|
819
|
+
background-color: var(--sf-elevation-3--surface) !important;
|
|
820
|
+
padding-block-start: calc(var(--sf-text-height-1) + var(--sf-ui-1--space-y-tightness-default) * 2 + var(--sf-a8) * 2) !important;
|
|
821
|
+
--sf-table-context-menu--max-block-size: 100dvh;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/* The sheet's way out: nothing outside it is left to press. */
|
|
825
|
+
.sf-table-context-layer[data-sf-sheet] .sf-table-sheet-close {
|
|
826
|
+
position: fixed;
|
|
827
|
+
inset-block-start: var(--sf-a8);
|
|
828
|
+
inset-inline-end: var(--sf-a8);
|
|
829
|
+
z-index: calc(var(--sf-z-index-9) + 1);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
@layer sf.states {
|
|
833
|
+
.sf-table-context-menu .sf-settings-context-menu-item:hover [data-keep] {
|
|
834
|
+
opacity: 1;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
@layer sf.overrides {
|
|
838
|
+
.sf-icon-button.sf-button-pinned .sf-icon {
|
|
839
|
+
--sf-button-pinned--color: var(--sf-primary);
|
|
840
|
+
--sf-icon--color: var(--sf-button-pinned--color);
|
|
841
|
+
}
|
|
842
|
+
.sf-table-context-menu .sf-context-menu-content {
|
|
843
|
+
/*--sf-context-menu-content--padding-bottom: var(--sf-space-2);*/
|
|
844
|
+
/*--sf-context-menu-content--padding-inline-end: var(--sf-space-2);*/
|
|
845
|
+
/*--sf-context-menu-content--padding-inline-start: var(--sf-space-2);*/
|
|
846
|
+
/*--sf-context-menu-content--padding-top: var(--sf-space-2);*/
|
|
847
|
+
/* These are lengths, not numbers. The menu's own axis is published as
|
|
848
|
+
`calc(space-x + 1px - var(--sf-context-menu-content--padding-inline-start))`,
|
|
849
|
+
and a unitless 0 makes that calc invalid, so the whole declaration is
|
|
850
|
+
dropped: the axis read as nothing on this panel, the menu's own items lost
|
|
851
|
+
their inset here, and this window's blocks had no axis to follow. A zero
|
|
852
|
+
with a unit costs nothing and keeps the arithmetic valid. */
|
|
853
|
+
--sf-context-menu-content--padding-bottom: 0px;
|
|
854
|
+
--sf-context-menu-content--padding-inline-end: 0px;
|
|
855
|
+
--sf-context-menu-content--padding-inline-start: 0px;
|
|
856
|
+
--sf-context-menu-content--padding-top: 0px;
|
|
857
|
+
}
|
|
858
|
+
.sf-table-context-menu .sf-settings-context-menu-item .sf-icon-button {
|
|
859
|
+
--sf-icon-button--padding-top: var(--sf-ui-1\/2--space-y-tightness-default);
|
|
860
|
+
--sf-icon-button--padding-bottom: var(--sf-ui-1\/2--space-y-tightness-default);
|
|
861
|
+
--sf-icon-button--padding-inline-start: var(--sf-ui-1\/2--space-y-tightness-default);
|
|
862
|
+
--sf-icon-button--padding-inline-end: var(--sf-ui-1\/2--space-y-tightness-default);
|
|
863
|
+
}
|
|
864
|
+
:is(sf-view-table, sf-table) > * .sf-search-input.sf-input.sf-input--size-1 .sf-input-field {
|
|
865
|
+
--sf-input-field--padding-bottom: var(
|
|
866
|
+
--sf-ui-1\/3--space-y-tightness-default
|
|
867
|
+
);
|
|
868
|
+
--sf-input-field--padding-top: var(--sf-ui-1\/3--space-y-tightness-default);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
:is(sf-view-table, sf-table)
|
|
872
|
+
> *
|
|
873
|
+
.sf-search-input.sf-input.sf-input--size-1
|
|
874
|
+
.sf-icon-button--size-1 {
|
|
875
|
+
--sf-icon-button--padding-bottom: var(
|
|
876
|
+
--sf-ui-1\/3--space-y-tightness-default
|
|
877
|
+
);
|
|
878
|
+
--sf-icon-button--padding-inline-end: var(
|
|
879
|
+
--sf-ui-1\/3--space-y-tightness-default
|
|
880
|
+
);
|
|
881
|
+
--sf-icon-button--padding-inline-start: var(
|
|
882
|
+
--sf-ui-1\/3--space-y-tightness-default
|
|
883
|
+
);
|
|
884
|
+
--sf-icon-button--padding-top: var(--sf-ui-1\/3--space-y-tightness-default);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
:is(sf-view-table, sf-table) .sf-avatar-label-group-container {
|
|
888
|
+
flex: 1 1 auto;
|
|
889
|
+
min-width: 0;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
:is(sf-view-table, sf-table) .sf-avatar-label-group-title {
|
|
893
|
+
overflow: hidden;
|
|
894
|
+
white-space: nowrap;
|
|
895
|
+
text-overflow: ellipsis;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/* Who the head sticks to follows who owns the scroll. A sticky element binds
|
|
899
|
+
to its nearest scroll container, so while the rows scroll in their own area
|
|
900
|
+
the head sticks to that area — right when the host bounds the table's
|
|
901
|
+
height, useless when it does not: the area never scrolls, the page does,
|
|
902
|
+
and the head leaves with it.
|
|
903
|
+
|
|
904
|
+
In `page` mode the area stops being a scroll container at all, so the head
|
|
905
|
+
resolves against the viewport. Measured rather than assumed: with the head
|
|
906
|
+
sticky and the page scrolled by 600px, `overflow: auto` leaves it at
|
|
907
|
+
-398px, `overflow-x: auto` with `overflow-y: clip` also leaves it at -398
|
|
908
|
+
because the clip axis computes to `hidden` and still makes a scroll
|
|
909
|
+
container, a bounded height leaves it at -398 as well, and only
|
|
910
|
+
`overflow: visible` holds it at 0. The cost is that a table wider than the
|
|
911
|
+
page scrolls the page sideways instead of scrolling inside its own box. */
|
|
912
|
+
/* One scroll on a list page (owner, 2026-10-15, from the approved
|
|
913
|
+
prototype): the page scrolls, never an area inside it, so in page mode the
|
|
914
|
+
table takes the height of its rows rather than shrinking into its box; the
|
|
915
|
+
area scrolls sideways only, its own bar replaced by the sticky rail under
|
|
916
|
+
it; the head is held under the toolbar by the table itself (it moves the
|
|
917
|
+
head while the page scrolls, see syncStickyHead), because a sticky head
|
|
918
|
+
binds to the area, which scrolls sideways and so is a scroll container. */
|
|
919
|
+
:is(sf-view-table, sf-table)[data-sf-page-scroll] > .sf-table,
|
|
920
|
+
:is(sf-view-table, sf-table)[data-sf-page-scroll] .table-wrap,
|
|
921
|
+
:is(sf-view-table, sf-table) .sf-table-scroll[data-sf-vertical-scroll="page"] {
|
|
922
|
+
flex: none;
|
|
923
|
+
min-block-size: auto;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
:is(sf-view-table, sf-table) .sf-table-scroll[data-sf-vertical-scroll="page"] {
|
|
927
|
+
overflow-x: auto;
|
|
928
|
+
overflow-y: hidden;
|
|
929
|
+
scrollbar-width: none;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
:is(sf-view-table, sf-table) .sf-table-scroll[data-sf-vertical-scroll="page"]::-webkit-scrollbar {
|
|
933
|
+
display: none;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/* The sideways bar, held at the bottom of the window while the table runs
|
|
937
|
+
past it, and in its place under the rows once they end. */
|
|
938
|
+
/* The area and its rail follow each other step for step: a smooth scroll
|
|
939
|
+
(the core's default) sent each half-way position back to the other, and
|
|
940
|
+
the two settled near the start. */
|
|
941
|
+
:is(sf-view-table, sf-table) .sf-table-scroll[data-sf-vertical-scroll="page"],
|
|
942
|
+
:is(sf-view-table, sf-table) .sf-table-rail {
|
|
943
|
+
scroll-behavior: auto;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
:is(sf-view-table, sf-table) .sf-table-rail {
|
|
947
|
+
position: sticky;
|
|
948
|
+
inset-block-end: 0;
|
|
949
|
+
z-index: 5;
|
|
950
|
+
overflow-x: auto;
|
|
951
|
+
overflow-y: hidden;
|
|
952
|
+
background-color: var(--sf-table-head--background-color, var(--sf-surface-0));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
:is(sf-view-table, sf-table) .sf-table-rail[hidden] {
|
|
956
|
+
display: none;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
:is(sf-view-table, sf-table) .sf-table-rail-track {
|
|
960
|
+
block-size: var(--sf-px);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/* Held: the toolbar at the top of what scrolls, the head under it. */
|
|
964
|
+
:is(sf-view-table, sf-table)[data-sf-sticky-head] .sf-table-toolbar {
|
|
965
|
+
position: sticky;
|
|
966
|
+
inset-block-start: 0;
|
|
967
|
+
z-index: 6;
|
|
968
|
+
background-color: var(--sf-table-head--background-color, var(--sf-surface-0));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
:is(sf-view-table, sf-table)[data-sf-page-scroll] thead {
|
|
972
|
+
position: relative;
|
|
973
|
+
z-index: 4;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
:is(sf-view-table, sf-table)[data-sf-head-stuck] thead {
|
|
977
|
+
box-shadow: 0 var(--sf-px) 0 var(--sf-outline-variant);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
/* A re-query over rows already on screen dims them and shows one spinner in
|
|
981
|
+
the middle of what the person can see. `sticky` centres it against the
|
|
982
|
+
scrollport rather than the table, so it stays in the middle of the visible
|
|
983
|
+
part of a table taller or wider than its box — the old loader replaced the
|
|
984
|
+
rows and sat in the top left corner of the result. */
|
|
985
|
+
/* The spinner over the rows takes no room: sticky, it stayed in the flow
|
|
986
|
+
with its own height and pushed the table down for as long as a sort or a
|
|
987
|
+
page took, and the table jumped down and back (owner, 2026-10-14). It is a
|
|
988
|
+
line of no height in the middle of the visible box, the spinner hung on
|
|
989
|
+
it. */
|
|
990
|
+
:is(sf-view-table, sf-table) .sf-table-busy {
|
|
991
|
+
position: sticky;
|
|
992
|
+
inset-block-start: 50%;
|
|
993
|
+
inset-inline-start: 50%;
|
|
994
|
+
translate: -50% 0;
|
|
995
|
+
inline-size: fit-content;
|
|
996
|
+
block-size: 0;
|
|
997
|
+
overflow: visible;
|
|
998
|
+
z-index: 4;
|
|
999
|
+
pointer-events: none;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
:is(sf-view-table, sf-table) .sf-table-busy > * {
|
|
1003
|
+
translate: 0 -50%;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/* The title column's link to the record: the table's one link style
|
|
1007
|
+
(above), in the title's weight. */
|
|
1008
|
+
:is(sf-view-table, sf-table) .sf-table-title-link {
|
|
1009
|
+
font-weight: var(--sf-font-weight-medium);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
:is(sf-view-table, sf-table) .sf-table-title-link:focus-visible {
|
|
1013
|
+
outline: var(--sf-focus--width) var(--sf-focus--style) var(--sf-focus--color);
|
|
1014
|
+
outline-offset: var(--sf-a2);
|
|
1015
|
+
border-radius: var(--sf-radius--ui);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
:is(sf-view-table, sf-table) tbody[data-busy] {
|
|
1019
|
+
opacity: 0.5;
|
|
1020
|
+
transition-property: opacity;
|
|
1021
|
+
transition-duration: var(--sf-duration-fast);
|
|
1022
|
+
transition-timing-function: var(--sf-animation);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1026
|
+
:is(sf-view-table, sf-table) tbody[data-busy] {
|
|
1027
|
+
transition-duration: 0s;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/* The pinned block keeps the box, the row menu and the title column in place
|
|
1032
|
+
while the rest of the row scrolls sideways. The edge of the block carries a
|
|
1033
|
+
soft shadow, and only while something is actually hidden behind it — the
|
|
1034
|
+
scroll handler sets the attribute. The shadow is drawn by the cell that
|
|
1035
|
+
ends the block, over the rows it overlaps, which is why it is a
|
|
1036
|
+
pseudo-element and not a border: a border would move the cell's content. */
|
|
1037
|
+
:is(sf-view-table, sf-table) .sf-table-scroll[data-sf-pin-scrolled] [data-sf-pin-edge]::after {
|
|
1038
|
+
content: "";
|
|
1039
|
+
position: absolute;
|
|
1040
|
+
inset-block: 0;
|
|
1041
|
+
inset-inline-end: calc(var(--sf-a4) * -1);
|
|
1042
|
+
inline-size: var(--sf-a4);
|
|
1043
|
+
pointer-events: none;
|
|
1044
|
+
/* `--sf-alpha-8` is the neutral ink of the current theme at 8%, so the
|
|
1045
|
+
shadow follows light and dark without a colour of its own. The direction
|
|
1046
|
+
is written physically because a gradient takes no logical keyword; the
|
|
1047
|
+
RTL pipeline mirrors it, as it does for the row's focus bar. */
|
|
1048
|
+
background-image: linear-gradient(to right, var(--sf-alpha-8), transparent);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/* A row says whether it is selected, so the look is a rule rather than a
|
|
1052
|
+
reading of the checkbox in its cell. `data-selected` and not
|
|
1053
|
+
`aria-selected`: this is a plain table, its rows are not grid rows, and the
|
|
1054
|
+
checkbox already carries the state for assistive technology — claiming it
|
|
1055
|
+
twice, once invalidly, would be worse than not claiming it here.
|
|
1056
|
+
|
|
1057
|
+
The colour sits on the row, not on its cells. A pinned cell has to be
|
|
1058
|
+
opaque or the rows it overlaps read through it, and it has to be opaque in
|
|
1059
|
+
the colour of its own row — painting it with the table surface, as the
|
|
1060
|
+
pinned placement first did, left the pinned columns white under a selected
|
|
1061
|
+
or a hovered row.
|
|
1062
|
+
|
|
1063
|
+
So a row is two layers: an opaque surface underneath and the state as a
|
|
1064
|
+
tint over it. Every state colour here is an alpha colour, so a pinned cell
|
|
1065
|
+
that simply inherited the result painted the same tint a second time and
|
|
1066
|
+
came out a shade darker than the rest of its row — which is what the
|
|
1067
|
+
consumer saw on a hovered row. Inheriting both layers paints each exactly
|
|
1068
|
+
once. */
|
|
1069
|
+
:is(sf-view-table, sf-table) tbody tr {
|
|
1070
|
+
background-color: var(--sf-table-row--background-color, var(--sf-surface-0));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
:is(sf-view-table, sf-table) .sf-table-scroll tbody [data-sf-pinned] {
|
|
1074
|
+
background-color: inherit;
|
|
1075
|
+
background-image: inherit;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/* The gear's «Вид» tab: the standard menu's rows -- its row height and
|
|
1079
|
+
inset -- a quiet heading over the density, a separator before the reach. */
|
|
1080
|
+
/* The tabs and the way out are the Framework panel's head now
|
|
1081
|
+
(sf-menu-context tabs, 2026-10-15). */
|
|
1082
|
+
.sf-table-view-settings .sf-table-settings-heading {
|
|
1083
|
+
padding-block-start: var(--sf-space-1\/4);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
.sf-table-view-settings .sf-table-settings-row {
|
|
1087
|
+
display: flex;
|
|
1088
|
+
align-items: center;
|
|
1089
|
+
gap: var(--sf-space-1\/2);
|
|
1090
|
+
min-block-size: calc(var(--sf-context-menu--line, var(--sf-text-height-1\/2)) + 2 * var(--sf-context-menu--space-y, var(--sf-a6)));
|
|
1091
|
+
padding-inline: var(--sf-context-menu--inset-start, var(--sf-a8)) var(--sf-context-menu--inset-end, var(--sf-a8));
|
|
1092
|
+
cursor: pointer;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.sf-table-view-settings .sf-table-settings-heading {
|
|
1096
|
+
padding-inline: var(--sf-context-menu--inset-start, var(--sf-a8));
|
|
1097
|
+
color: var(--sf-on-surface-variant);
|
|
1098
|
+
font-size: var(--sf-text-size-1\/3);
|
|
1099
|
+
line-height: var(--sf-text-height-1\/3);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.sf-table-view-settings .sf-table-settings-separator {
|
|
1103
|
+
block-size: var(--sf-px);
|
|
1104
|
+
margin-block: var(--sf-space-1\/4);
|
|
1105
|
+
background-color: var(--sf-outline-variant);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/* The person's view of the table (the gear's «Вид», owner 2026-10-14),
|
|
1109
|
+
kept as a view preference and written on the host as data attributes.
|
|
1110
|
+
Density moves the rows' vertical space one ladder step either way. */
|
|
1111
|
+
:is(sf-view-table, sf-table)[data-sf-view-density="compact"] > * .table > :not(caption) > * > * {
|
|
1112
|
+
padding-block: var(--sf-space-1\/2);
|
|
1113
|
+
}
|
|
1114
|
+
:is(sf-view-table, sf-table)[size="1/2"][data-sf-view-density="compact"] > * .table > :not(caption) > * > * {
|
|
1115
|
+
padding-block: var(--sf-space-1\/4);
|
|
1116
|
+
}
|
|
1117
|
+
:is(sf-view-table, sf-table)[data-sf-view-density="spacious"] > * .table > :not(caption) > * > * {
|
|
1118
|
+
padding-block: var(--sf-space-2);
|
|
1119
|
+
}
|
|
1120
|
+
:is(sf-view-table, sf-table)[size="1/2"][data-sf-view-density="spacious"] > * .table > :not(caption) > * > * {
|
|
1121
|
+
padding-block: var(--sf-space-1\/2);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/* Alternate rows on the container surface, under the hover and selection
|
|
1125
|
+
layers, which are images over it. Opaque: the container surface is an
|
|
1126
|
+
alpha colour, and a pinned cell inherits its row's colour, so the rows
|
|
1127
|
+
scrolling under the pinned columns read through them (owner,
|
|
1128
|
+
2026-10-16). The same tint, mixed with the surface it lies on. */
|
|
1129
|
+
:is(sf-view-table, sf-table)[data-sf-view-stripes] tbody tr:nth-child(even) {
|
|
1130
|
+
--sf-table-row--background-color: color-mix(in srgb,
|
|
1131
|
+
light-dark(var(--sf-neutral-50), var(--sf-neutral-90)) 16%, var(--sf-surface-0));
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/* A line between the columns, the table's own line colour. */
|
|
1135
|
+
:is(sf-view-table, sf-table)[data-sf-view-lines] :is(th, td):not(:last-child) {
|
|
1136
|
+
border-inline-end: var(--sf-px) solid var(--sf-outline-variant);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/* One line a cell: a long text ends in «…» rather than taking three. */
|
|
1140
|
+
:is(sf-view-table, sf-table)[data-sf-view-single-line] {
|
|
1141
|
+
--sf-table-cell--line-clamp: 1;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/* A colour option in a filter window: its swatch, a text line square. */
|
|
1145
|
+
.sf-table-filter-swatch {
|
|
1146
|
+
flex: none;
|
|
1147
|
+
inline-size: var(--sf-text-height-1\/2);
|
|
1148
|
+
block-size: var(--sf-text-height-1\/2);
|
|
1149
|
+
border-radius: var(--sf-radius--ui);
|
|
1150
|
+
box-shadow: inset 0 0 0 var(--sf-px) var(--sf-outline-variant);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
:is(sf-view-table, sf-table) tbody tr:hover {
|
|
1154
|
+
background-image: linear-gradient(
|
|
1155
|
+
var(--sf-surface-container-hover),
|
|
1156
|
+
var(--sf-surface-container-hover)
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
:is(sf-view-table, sf-table) tbody tr[data-selected] {
|
|
1161
|
+
background-image: linear-gradient(
|
|
1162
|
+
var(--sf-primary-container),
|
|
1163
|
+
var(--sf-primary-container)
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/* The row the person is on carries `data-current` and takes focus, and shows
|
|
1168
|
+
nothing of its own. It used to draw a bar in its first cell, and the
|
|
1169
|
+
consumer found that the bar outlived what it meant: after a box was
|
|
1170
|
+
unticked the bar stayed, and a bar beside a row reads as a mark. Where the
|
|
1171
|
+
person is is answered by the focus ring instead, and only when the keyboard
|
|
1172
|
+
is what put them there.
|
|
1173
|
+
|
|
1174
|
+
The ring is one ring around the row, drawn by a pseudo-element above the
|
|
1175
|
+
pinned cells. A plain outline on the row is painted over by them — a cell
|
|
1176
|
+
is a child of the row, so its background covers the parent's outline — and
|
|
1177
|
+
that is what the consumer saw: blue segments across the loose cells only,
|
|
1178
|
+
appearing the moment a modifier key switched the browser into keyboard
|
|
1179
|
+
modality. */
|
|
1180
|
+
/* Only a row that has the keyboard focus is positioned, because only then is
|
|
1181
|
+
there a ring to hang on it. Positioning every row made each one a stacking
|
|
1182
|
+
context of its own, and a consumer saw pinned cells left unpainted while
|
|
1183
|
+
scrolling a table with two pinned columns — the kind of compositing
|
|
1184
|
+
difference that costs nothing to avoid. */
|
|
1185
|
+
:is(sf-view-table, sf-table) tbody tr:focus-visible {
|
|
1186
|
+
position: relative;
|
|
1187
|
+
outline: none;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
:is(sf-view-table, sf-table) tbody tr:focus-visible::after {
|
|
1191
|
+
content: "";
|
|
1192
|
+
position: absolute;
|
|
1193
|
+
inset: 0;
|
|
1194
|
+
z-index: 3;
|
|
1195
|
+
pointer-events: none;
|
|
1196
|
+
outline: var(--sf-focus--width) var(--sf-focus--style) var(--sf-focus--color);
|
|
1197
|
+
outline-offset: calc(var(--sf-focus--width) * -1);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
:is(sf-view-table, sf-table) tbody tr[data-selected]:hover {
|
|
1201
|
+
background-image: linear-gradient(
|
|
1202
|
+
var(--sf-primary-container-hover),
|
|
1203
|
+
var(--sf-primary-container-hover)
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/* Sorting is answered in the header, not by shading the column: a sorted
|
|
1208
|
+
column keeps the background of every other one. */
|
|
1209
|
+
:is(sf-view-table, sf-table) th:hover .sf-table-sort-icon {
|
|
1210
|
+
/* A glyph is non-text graphics, and WCAG 1.4.11 asks 3:1 of it.
|
|
1211
|
+
--sf-on-surface-muted is 3.18:1 light and 2.92:1 dark, so it sat on the
|
|
1212
|
+
line in one theme and under it in the other. --sf-on-surface-variant is
|
|
1213
|
+
the Framework's own paler-than-text role, 6.47 and 5.95, and the one a
|
|
1214
|
+
label already reads in (owner, 2026-10-08). */
|
|
1215
|
+
--sf-icon--color: var(--sf-on-surface-variant);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
:is(sf-view-table, sf-table) th[aria-sort]:not([aria-sort="none"]) .sf-table-sort-button {
|
|
1219
|
+
color: var(--sf-primary);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
:is(sf-view-table, sf-table) th[aria-sort]:not([aria-sort="none"]) .sf-table-sort-icon {
|
|
1223
|
+
--sf-icon--color: var(--sf-primary);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/* The mark under a sorted header is drawn rather than bordered, so it does not
|
|
1227
|
+
move the cell or fight the separator the resize handle uses. */
|
|
1228
|
+
:is(sf-view-table, sf-table) th[aria-sort]:not([aria-sort="none"]) {
|
|
1229
|
+
position: relative;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
:is(sf-view-table, sf-table) th[aria-sort]:not([aria-sort="none"])::after {
|
|
1233
|
+
content: "";
|
|
1234
|
+
position: absolute;
|
|
1235
|
+
inset-inline: 0;
|
|
1236
|
+
inset-block-end: 0;
|
|
1237
|
+
block-size: var(--sf-a2);
|
|
1238
|
+
background-color: var(--sf-primary);
|
|
1239
|
+
pointer-events: none;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
@media (forced-colors: active) {
|
|
1243
|
+
:is(sf-view-table, sf-table) th[aria-sort]:not([aria-sort="none"])::after {
|
|
1244
|
+
background-color: Highlight;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
:is(sf-view-table, sf-table) tbody tr[data-selected] > td {
|
|
1248
|
+
background-color: Highlight;
|
|
1249
|
+
color: HighlightText;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/* The guide is one line, 1px in the colour of every other line in the table —
|
|
1254
|
+
not the primary colour, which would read as a selection. It runs through the
|
|
1255
|
+
rows only: the head keeps its own short separator below. */
|
|
1256
|
+
:is(sf-view-table, sf-table) .sf-table-resize-guide {
|
|
1257
|
+
position: absolute;
|
|
1258
|
+
inline-size: var(--sf-px);
|
|
1259
|
+
background-color: var(--sf-outline-variant);
|
|
1260
|
+
pointer-events: none;
|
|
1261
|
+
z-index: 4;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
:is(sf-view-table, sf-table) .sf-table-resize-guide[hidden] {
|
|
1265
|
+
display: none;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
@media (forced-colors: active) {
|
|
1269
|
+
:is(sf-view-table, sf-table) .sf-table-resize-guide {
|
|
1270
|
+
background-color: CanvasText;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/* Where a dragged row lands in a list put in order by hand (row-reorder): a
|
|
1276
|
+
line on the edge it will stand at. */
|
|
1277
|
+
:is(sf-view-table, sf-table) tbody tr[data-sf-drop-before] > td {
|
|
1278
|
+
box-shadow: inset 0 calc(2 * var(--sf-px)) 0 var(--sf-primary);
|
|
1279
|
+
}
|
|
1280
|
+
:is(sf-view-table, sf-table) tbody tr[data-sf-drop-after] > td {
|
|
1281
|
+
box-shadow: inset 0 calc(-2 * var(--sf-px)) 0 var(--sf-primary);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/* Many entries (standard 1.3.8 §18.4): the spacer rows that stand for the
|
|
1285
|
+
rows not drawn hold their height and nothing else. */
|
|
1286
|
+
@layer sf.components {
|
|
1287
|
+
:is(sf-view-table, sf-table) tbody tr.sf-table-window-spacer,
|
|
1288
|
+
:is(sf-view-table, sf-table) tbody tr.sf-table-window-spacer:hover {
|
|
1289
|
+
background: none;
|
|
1290
|
+
}
|
|
1291
|
+
:is(sf-view-table, sf-table) tbody tr.sf-table-window-spacer > td {
|
|
1292
|
+
padding: 0;
|
|
1293
|
+
border: 0;
|
|
1294
|
+
}
|
|
1295
|
+
/* The spacers already keep every drawn row where it was: the browser's own
|
|
1296
|
+
scroll anchoring would move the page a second time when the range moves. */
|
|
1297
|
+
:is(sf-view-table, sf-table) tbody[data-sf-window],
|
|
1298
|
+
:is(sf-view-table, sf-table) tbody[data-sf-window] > tr {
|
|
1299
|
+
overflow-anchor: none;
|
|
1300
|
+
}
|
|
1301
|
+
}
|