spiderly 19.11.8 → 19.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,5 @@
1
+ @use "../../styles/layout/mixins" as *;
2
+
1
3
  .table-header {
2
4
  display: flex;
3
5
  justify-content: space-between;
@@ -48,12 +50,9 @@
48
50
  cursor: pointer;
49
51
  }
50
52
  .column-chooser-reset {
53
+ @include bare-button();
51
54
  margin-top: 4px;
52
- padding: 0;
53
- border: none;
54
- background: none;
55
55
  color: var(--p-primary-color);
56
- cursor: pointer;
57
56
  align-self: flex-start;
58
57
  font-size: 0.875rem;
59
58
 
@@ -62,8 +61,44 @@
62
61
  }
63
62
  }
64
63
 
64
+ // Teleported with its popover, so top level — the same rule the chooser follows above.
65
+ .column-menu {
66
+ display: flex;
67
+ flex-direction: column;
68
+ min-width: 11rem;
69
+ }
70
+
71
+ .column-menu-item {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 8px;
75
+ // The label takes the slack so a trailing check sits at the far edge rather than beside the
76
+ // text, where it would read as part of it.
77
+ .column-menu-item-label {
78
+ flex: 1;
79
+ }
80
+ @include bare-button();
81
+ text-align: left;
82
+ padding: 6px 10px;
83
+ border-radius: var(--p-content-border-radius);
84
+
85
+ &:hover:not(:disabled) {
86
+ background: var(--p-content-hover-background);
87
+ }
88
+
89
+ &:disabled {
90
+ color: var(--p-text-muted-color);
91
+ cursor: default;
92
+ }
93
+ }
94
+
65
95
  :host {
66
96
  // In-template content only — rules for teleported overlay content go at top level (see above).
97
+
98
+ // onPageChange scrolls this wrapper back into view; the mixin carries the offset contract.
99
+ .spiderly-table-container {
100
+ @include scroll-target-below-top-chrome();
101
+ }
67
102
  // The `.clickable` affordance is shared: clickable rows (navigateOnRowClick) and opt-in cells
68
103
  // (Column.onCellClick) both get a pointer cursor; cells additionally underline on hover.
69
104
  ::ng-deep .clickable {
@@ -73,11 +108,142 @@
73
108
  text-decoration: underline;
74
109
  }
75
110
 
111
+ // Only ink on hover or focus: a chevron on every header at rest is noise across nineteen of
112
+ // them, and the header already carries a name and a sort icon.
113
+ .column-menu-button {
114
+ @include bare-button();
115
+ color: var(--p-text-muted-color);
116
+ padding: 0 2px;
117
+ opacity: 0;
118
+ font-size: 0.75rem;
119
+ }
120
+
121
+ th:hover .column-menu-button,
122
+ .column-menu-button:focus-visible {
123
+ opacity: 1;
124
+ }
125
+
126
+ // Tabs, above the bar: a view names the question, the bar shows what it resolved to. Shopify
127
+ // puts saved order views in the same place for the same reason.
128
+ .table-views {
129
+ display: flex;
130
+ flex-wrap: wrap;
131
+ gap: 4px;
132
+ padding-top: 8px;
133
+ }
134
+
135
+ .table-view {
136
+ @include bare-button();
137
+ color: var(--p-text-muted-color);
138
+ padding: 4px 10px;
139
+ border-radius: var(--p-content-border-radius);
140
+
141
+ &:hover {
142
+ background: var(--p-content-hover-background);
143
+ }
144
+ }
145
+
146
+ .table-view-active {
147
+ color: var(--p-primary-color);
148
+ font-weight: 600;
149
+ background: var(--p-content-hover-background);
150
+ }
151
+
152
+ // The left edge that survives a horizontal scroll. Geometry only here: the opaque background
153
+ // lives on the BODY cells below, because the header th already gets an opaque
154
+ // header.cell.background from PrimeNG's own th rule. Painting the row palette on the whole
155
+ // class put the pinned th on the row colour and let the hover rule tint it whenever the pointer
156
+ // crossed ANY header cell — the locked column reacting on its own, again ("na hover ostalih
157
+ // hoveruje i locked kolone", Filip, 2026-09-05) — and overrode PrimeNG's per-cell sortable
158
+ // header hover, which is the hover a th should have.
159
+ .frozen-column {
160
+ position: sticky;
161
+ z-index: 1;
162
+ }
163
+
164
+ // Body cells: the background must be OPAQUE and must track hover and striping, or rows show
165
+ // through the pinned cells as they pass underneath — which is the one thing that makes a sticky
166
+ // column look broken rather than merely unstyled. It inherits rather than naming a colour so
167
+ // PrimeNG's own row states keep working.
168
+ td.frozen-column {
169
+ background: var(--p-datatable-row-background, var(--p-content-background));
170
+ // The row fades between its two backgrounds and this repainted instantly, so the pinned
171
+ // columns changed shade a beat before the rest of the row — which reads as the locked column
172
+ // reacting on its own rather than as one row highlighting (Filip, on /tags). Same duration
173
+ // token PrimeNG's own row uses, so the two are one movement.
174
+ transition: background-color var(--p-transition-duration, 0.2s);
175
+ }
176
+
177
+ tr:hover > td.frozen-column {
178
+ background: var(--p-datatable-row-hover-background, inherit);
179
+ }
180
+
181
+ // The seam. NN/g calls for a shadow so the pinned columns read as floating ABOVE the rest
182
+ // rather than as the first two columns of a table that happens not to move.
183
+ //
184
+ // `:last-of-type` shipped here first and drew NOTHING: it means "last th among its siblings",
185
+ // and the frozen cells are the FIRST ones — it was written for "the last frozen column", which
186
+ // is a concept the markup never expressed. The class does.
187
+ .frozen-column-last {
188
+ box-shadow: 2px 0 4px -2px rgb(0 0 0 / 15%);
189
+ }
190
+
191
+ // The grab handle on a header's right edge. The th is position:relative through PrimeNG's own
192
+ // styles; the strip is wider than the line it draws so it can actually be hit.
193
+ th {
194
+ position: relative;
195
+ }
196
+
197
+ .column-resizer {
198
+ position: absolute;
199
+ top: 0;
200
+ right: 0;
201
+ width: 8px;
202
+ height: 100%;
203
+ cursor: col-resize;
204
+ user-select: none;
205
+ }
206
+
207
+ // The select-all counter. A smaller step than the body text because it is a parenthetical, and
208
+ // because it is what decides how narrow the checkbox column can be.
209
+ .selection-count {
210
+ font-size: 0.75rem;
211
+ color: var(--p-text-muted-color);
212
+ }
213
+
76
214
  // Belt to onSelectionCellMouseDown's preventDefault (shift+click text-selection suppression).
77
215
  td.selection-cell {
78
216
  user-select: none;
79
217
  }
80
218
 
219
+ // The operator's per-column wrap (header menu) travels as inherited custom properties, because
220
+ // the library cannot style a projected spiderlyCellTemplate directly (CLAUDE.md → Column
221
+ // widths). Unset, the var()s below fall back to the clamp; a wrapped td flips both, and any
222
+ // consumer clamp written against the same vars follows — that is the whole contract. Only these
223
+ // two: text-overflow needs no property, since it has no effect once overflow is visible.
224
+ td.cell-wrap {
225
+ --spiderly-cell-overflow: visible;
226
+ --spiderly-cell-white-space: normal;
227
+ }
228
+
229
+ // The cost the fixed layout would otherwise hand over: text no longer widens its column, so
230
+ // it wraps inside it and a long value grows the row — which pushes every row below it, a worse
231
+ // shift than the horizontal one the fixed layout removes. One line, ellipsis, whole value still
232
+ // in the DOM to select and copy. The clamp stays the default precisely because a row that grows
233
+ // by content costs more than the line saves — wrapping is a choice someone makes about one
234
+ // column (td.cell-wrap above), not a shape every table inherits.
235
+ .cell-text {
236
+ display: block;
237
+ overflow: var(--spiderly-cell-overflow, hidden);
238
+ text-overflow: ellipsis;
239
+ white-space: var(--spiderly-cell-white-space, nowrap);
240
+ }
241
+
242
+ // PrimeNG-rendered markup, so ::ng-deep is the only way in; the mixin carries the contract.
243
+ ::ng-deep .p-datatable-mask {
244
+ @include table-pending-veil();
245
+ }
246
+
81
247
  ::ng-deep {
82
248
  .p-datatable-thead {
83
249
  position: unset !important;
@@ -1,3 +1,19 @@
1
+ @use "../../styles/layout/mixins" as *;
2
+
3
+ :host {
4
+ // onPageChange scrolls this wrapper back into view; the mixin carries the offset contract.
5
+ .spiderly-table-container {
6
+ @include scroll-target-below-top-chrome();
7
+ }
8
+
9
+ // PrimeNG-rendered markup, so ::ng-deep is the only way in. Kept under :host deliberately:
10
+ // the block further down is at SCSS top level, which strips encapsulation entirely and leaks
11
+ // its rules to every component in the consuming app. Do not extend that one.
12
+ ::ng-deep .p-datatable-mask {
13
+ @include table-pending-veil();
14
+ }
15
+ }
16
+
1
17
  ::ng-deep {
2
18
  .p-datatable-tbody {
3
19
  display: flex;
@@ -0,0 +1,163 @@
1
+ @use "../styles/layout/mixins" as *;
2
+
3
+ // Not teleported, so `:host` scoping holds here — unlike the column chooser, whose rules must sit
4
+ // at top level because PrimeNG moves its popover into document.body (Angular/CLAUDE.md → overlay
5
+ // styling). Colours come from the PrimeNG theme's variables so the bar follows the admin's theme
6
+ // rather than carrying a second palette.
7
+ :host {
8
+ display: block;
9
+ }
10
+
11
+
12
+ .filter-bar {
13
+ display: flex;
14
+ flex-wrap: wrap;
15
+ align-items: center;
16
+ gap: 8px;
17
+ padding: 8px 0 0;
18
+ }
19
+
20
+ .filter-chip {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ gap: 2px;
24
+ border: 1px solid var(--p-content-border-color);
25
+ border-radius: 999px;
26
+ background: var(--p-content-background);
27
+ // The chip is two buttons in one pill; the padding lives on them so both halves are fully
28
+ // clickable rather than leaving a dead ring around the text.
29
+ padding: 0;
30
+ font-size: 0.875rem;
31
+ line-height: 1.4;
32
+ }
33
+
34
+ .filter-chip-edit {
35
+ @include bare-button();
36
+ display: inline-flex;
37
+ align-items: center;
38
+ gap: 4px;
39
+ padding: 4px 4px 4px 10px;
40
+ border-radius: 999px 0 0 999px;
41
+
42
+ &:hover .filter-chip-value {
43
+ text-decoration: underline;
44
+ }
45
+ }
46
+
47
+ .filter-chip-label {
48
+ color: var(--p-text-muted-color);
49
+ }
50
+
51
+ .filter-chip-value {
52
+ font-weight: 600;
53
+ }
54
+
55
+ .filter-chip-remove {
56
+ @include bare-button();
57
+ color: var(--p-text-muted-color);
58
+ font-size: 1.05rem;
59
+ line-height: 1;
60
+ padding: 4px 9px 5px 4px;
61
+ border-radius: 0 999px 999px 0;
62
+
63
+ &:hover {
64
+ color: var(--p-primary-color);
65
+ }
66
+ }
67
+
68
+ // PrimeNG's button owns the look; only placement belongs here.
69
+ .filter-add {
70
+ flex: none;
71
+ }
72
+
73
+ // The add menu is teleported into document.body with its popover, so its rules must sit at TOP
74
+ // LEVEL — `:host` nesting dies on teleport, exactly as the column chooser's do (Angular/CLAUDE.md
75
+ // → overlay styling). The editor is in-template and could be nested, but the two are kept together
76
+ // so nobody has to remember which is which.
77
+ .filter-add-menu {
78
+ display: flex;
79
+ flex-direction: column;
80
+ align-items: stretch;
81
+ gap: 2px;
82
+ min-width: 14rem;
83
+ // Nineteen filters is taller than a popover should ever be; the list scrolls and the search box
84
+ // above it does not move with it.
85
+ max-height: 20rem;
86
+ overflow-y: auto;
87
+ }
88
+
89
+ .filter-add-search {
90
+ margin-bottom: 4px;
91
+ position: sticky;
92
+ top: 0;
93
+ }
94
+
95
+ .filter-add-option {
96
+ @include bare-button();
97
+ text-align: left;
98
+ padding: 6px 10px;
99
+ border-radius: var(--p-content-border-radius);
100
+
101
+ &:hover {
102
+ background: var(--p-content-hover-background);
103
+ }
104
+ }
105
+
106
+ // Its own row under the chips, full width, so the controls have room and always appear in one
107
+ // predictable place.
108
+ // Every control here is sized through its own `size="small"` input, never a hand-written
109
+ // `p-inputtext-sm`: each PrimeNG component has its OWN small class (p-select-sm,
110
+ // p-multiselect-sm), so the input class silently did nothing on a select and left it a head
111
+ // taller than the box beside it.
112
+ .filter-editor {
113
+ display: flex;
114
+ margin-top: 8px;
115
+ flex-wrap: wrap;
116
+ align-items: center;
117
+ gap: 8px;
118
+ border: 1px solid var(--p-content-border-color);
119
+ border-radius: var(--p-content-border-radius);
120
+ background: var(--p-content-background);
121
+ padding: 8px;
122
+ }
123
+
124
+ .filter-editor-label {
125
+ font-weight: 600;
126
+ }
127
+
128
+ .filter-editor-tick {
129
+ display: inline-flex;
130
+ align-items: center;
131
+ gap: 4px;
132
+ cursor: pointer;
133
+ }
134
+
135
+ .filter-bar-count {
136
+ color: var(--p-text-muted-color);
137
+ font-size: 0.875rem;
138
+ // Pushed away from the chips it summarises, not crowded against the last one.
139
+ margin-left: 4px;
140
+ }
141
+
142
+ .sort-chip {
143
+ display: inline-flex;
144
+ align-items: center;
145
+ gap: 4px;
146
+ color: var(--p-text-muted-color);
147
+ font-size: 0.875rem;
148
+ }
149
+
150
+ .sort-chip-key {
151
+ // The direction arrow is the non-colour channel; the priority is the reading order, which is
152
+ // the thing the scattered header icons could never show.
153
+ color: var(--p-text-color);
154
+ }
155
+
156
+ .filter-bar-clear {
157
+ // Pushed to the far end, away from the chips: a destructive action next to the x it resembles
158
+ // is how a single chip gets removed by someone reaching for all of them. It is also the LAST
159
+ // child, because an auto margin here pushes everything after it to the right edge too — which
160
+ // is how the editor ended up across the screen from the click it answered.
161
+ margin-left: auto;
162
+ flex: none;
163
+ }
@@ -4,7 +4,7 @@
4
4
  display: flex;
5
5
  flex-direction: column;
6
6
  min-height: 100vh;
7
- padding: 7rem 2rem 2rem 4rem;
7
+ padding: calc(var(--spiderly-topbar-height) + 2rem) 2rem 2rem 4rem;
8
8
  transition: margin-left $transitionDuration;
9
9
  }
10
10
 
@@ -4,10 +4,24 @@
4
4
  box-sizing: border-box;
5
5
  }
6
6
 
7
+ // Declared through :where() so both vars carry ZERO specificity: a consumer's own
8
+ // `html { --spiderly-… }` then wins wherever it sits in the cascade, instead of the override
9
+ // working or not depending on stylesheet order. Full contract, and the fallback rule
10
+ // components must follow: Angular/CLAUDE.md → fixed-chrome offset.
11
+ :where(html) {
12
+ // Two names because they are two quantities that merely coincide in THIS shell: the topbar's
13
+ // own height, and how much fixed chrome covers the top of the viewport. A shell that pins a
14
+ // second bar under the topbar raises the inset without stretching the topbar.
15
+ --spiderly-topbar-height: 5rem;
16
+ --spiderly-viewport-top-inset: var(--spiderly-topbar-height);
17
+ }
18
+
7
19
  html {
8
20
  height: 100%;
9
21
  font-size: $scale;
10
- scroll-behavior: smooth;
22
+
23
+ // No `scroll-behavior` here, deliberately (2026-08-29) — the argument against re-adding it
24
+ // is in Angular/CLAUDE.md → fixed-chrome offset.
11
25
  }
12
26
 
13
27
  body {
@@ -8,7 +8,7 @@
8
8
  z-index: 999;
9
9
  overflow-y: auto;
10
10
  user-select: none;
11
- top: 7rem;
11
+ top: calc(var(--spiderly-topbar-height) + 2rem);
12
12
  left: 2rem;
13
13
  transition:
14
14
  transform $transitionDuration,
@@ -1,3 +1,16 @@
1
+ // A <button> stripped to bare text — the reset behind the library chrome's chips, view tabs,
2
+ // menu rows and plain-text actions. Only the reset lives here: colour, padding, radius and hover
3
+ // stay at the call site (declared AFTER the include, so they win), because what a bare button
4
+ // looks like is per surface — that it stops looking like a UA button is not.
5
+ @mixin bare-button() {
6
+ border: none;
7
+ background: none;
8
+ color: inherit;
9
+ font: inherit;
10
+ cursor: pointer;
11
+ padding: 0;
12
+ }
13
+
1
14
  @mixin focused() {
2
15
  outline: 0 none;
3
16
  outline-offset: 0;
@@ -11,3 +24,36 @@
11
24
  transition: box-shadow 0.2s;
12
25
  box-shadow: inset 0 0 0 0.2rem var(--p-primary-400);
13
26
  }
27
+
28
+ // Marks an element as a scroll-into-view target that must clear the shell's fixed top chrome.
29
+ // This is the ONE declaration of that offset: scrollElementIntoViewIfAboveViewport reads the
30
+ // computed value back rather than taking a number, so nothing in TypeScript knows the shell's
31
+ // dimensions. The 0px fallback is what stops a consumer running its own shell (no
32
+ // --spiderly-viewport-top-inset declared) from getting a phantom gap above the element.
33
+ @mixin scroll-target-below-top-chrome() {
34
+ scroll-margin-top: calc(var(--spiderly-viewport-top-inset, 0px) + 1rem);
35
+ }
36
+
37
+ // The overlay PrimeNG mounts over a table while a fetch is in flight (.p-datatable-mask).
38
+ // Its own theme token is a modal-strength scrim -- rgba(0,0,0,0.4) light, 0.6 dark -- which
39
+ // makes the rows underneath unreadable. Since the whole point of keeping the previous page on
40
+ // screen during a refetch is that it stays READABLE, the scrim is replaced by a light veil.
41
+ // Three parts, each load-bearing:
42
+ // - `color`, because SpinnerIcon paints fill="currentColor". Overriding only the background
43
+ // leaves a near-white spinner on a near-white veil.
44
+ // - `flex-start` plus a top pad, because the mask spans the whole datatable root: a centred
45
+ // spinner on a long list sits far below the fold, worst right after a page change has
46
+ // scrolled the reader to the table's top. The pad is deliberately approximate -- it only has
47
+ // to land the spinner inside the first screenful, not on any particular row. `position:
48
+ // sticky` was tried and cannot work here: the icon's containing block IS the mask.
49
+ // - A fallback on every var: Karma loads no PrimeNG theme, and a consumer need not either.
50
+ @mixin table-pending-veil() {
51
+ background: color-mix(
52
+ in srgb,
53
+ var(--p-content-background, #ffffff) 65%,
54
+ transparent
55
+ );
56
+ color: var(--p-primary-color, #334155);
57
+ align-items: flex-start;
58
+ padding-top: 6rem;
59
+ }
@@ -3,7 +3,7 @@
3
3
 
4
4
  .layout-topbar {
5
5
  position: fixed;
6
- height: 5rem;
6
+ height: var(--spiderly-topbar-height);
7
7
  z-index: 997;
8
8
  left: 0;
9
9
  top: 0;
@@ -136,7 +136,7 @@
136
136
  border-radius: var(--p-content-border-radius);
137
137
  padding: 1rem;
138
138
  right: 2rem;
139
- top: 5rem;
139
+ top: var(--spiderly-topbar-height);
140
140
  min-width: 15rem;
141
141
  display: none;
142
142
  -webkit-animation: scalein 0.15s linear;
@@ -181,7 +181,7 @@
181
181
  border-radius: var(--p-content-border-radius);
182
182
  padding: 1rem;
183
183
  right: 2rem;
184
- top: 5rem;
184
+ top: var(--spiderly-topbar-height);
185
185
  min-width: 15rem;
186
186
  -webkit-animation: scalein 0.15s linear;
187
187
  animation: scalein 0.15s linear;
@@ -196,7 +196,7 @@
196
196
  @media (max-width: 991px) {
197
197
  width: 210px;
198
198
  right: 1rem;
199
- top: 4.5rem;
199
+ top: calc(var(--spiderly-topbar-height) - 0.5rem);
200
200
  }
201
201
 
202
202
  .layout-topbar-profile-dropdown-button {