spiderly 19.11.9 → 19.12.1
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/agent/docs/angular-customization/index.md +56 -14
- package/agent/docs/angular-customization/references/helper-functions.generated.md +1 -0
- package/agent/docs/e2e-testing/index.md +10 -15
- package/agent/skills/add-entity/SKILL.md +12 -1
- package/fesm2022/spiderly.mjs +2206 -449
- package/fesm2022/spiderly.mjs.map +1 -1
- package/lib/components/spiderly-data-table/spiderly-data-table.component.d.ts +512 -141
- package/lib/directives/spiderly-filter-template.directive.d.ts +34 -0
- package/lib/directives/spiderly-overflow-title.directive.d.ts +33 -0
- package/lib/entities/filter-rule.d.ts +5 -7
- package/lib/filters/allowed-operators.d.ts +24 -0
- package/lib/filters/filter-store.d.ts +264 -0
- package/lib/filters/spiderly-filter-bar.component.d.ts +132 -0
- package/lib/services/helper-functions.d.ts +8 -0
- package/package.json +1 -1
- package/public-api.d.ts +5 -0
- package/styles/components/spiderly-data-table/spiderly-data-table.component.scss +150 -18
- package/styles/filters/spiderly-filter-bar.component.scss +163 -0
- package/styles/styles/layout/_mixins.scss +13 -0
|
@@ -6,15 +6,6 @@
|
|
|
6
6
|
align-items: center;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
// The active-filter header icon (the projected filtericon template). Bare on purpose:
|
|
10
|
-
// scoping it under PrimeNG's .p-datatable-column-filter-button would compile to a
|
|
11
|
-
// descendant selector requiring OUR _ngcontent on the button, which is PrimeNG-rendered
|
|
12
|
-
// markup — the rule would die silently. Only elements authored in this component's
|
|
13
|
-
// template can match, and the icon is the only .pi-filter-fill here.
|
|
14
|
-
.pi-filter-fill {
|
|
15
|
-
color: var(--p-primary-color);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
9
|
@media (max-width: 640px) {
|
|
19
10
|
.table-header {
|
|
20
11
|
display: flex;
|
|
@@ -59,12 +50,9 @@
|
|
|
59
50
|
cursor: pointer;
|
|
60
51
|
}
|
|
61
52
|
.column-chooser-reset {
|
|
53
|
+
@include bare-button();
|
|
62
54
|
margin-top: 4px;
|
|
63
|
-
padding: 0;
|
|
64
|
-
border: none;
|
|
65
|
-
background: none;
|
|
66
55
|
color: var(--p-primary-color);
|
|
67
|
-
cursor: pointer;
|
|
68
56
|
align-self: flex-start;
|
|
69
57
|
font-size: 0.875rem;
|
|
70
58
|
|
|
@@ -73,6 +61,37 @@
|
|
|
73
61
|
}
|
|
74
62
|
}
|
|
75
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
|
+
|
|
76
95
|
:host {
|
|
77
96
|
// In-template content only — rules for teleported overlay content go at top level (see above).
|
|
78
97
|
|
|
@@ -89,22 +108,135 @@
|
|
|
89
108
|
text-decoration: underline;
|
|
90
109
|
}
|
|
91
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
|
+
|
|
92
214
|
// Belt to onSelectionCellMouseDown's preventDefault (shift+click text-selection suppression).
|
|
93
215
|
td.selection-cell {
|
|
94
216
|
user-select: none;
|
|
95
217
|
}
|
|
96
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
|
+
|
|
97
229
|
// The cost the fixed layout would otherwise hand over: text no longer widens its column, so
|
|
98
230
|
// it wraps inside it and a long value grows the row — which pushes every row below it, a worse
|
|
99
231
|
// shift than the horizontal one the fixed layout removes. One line, ellipsis, whole value still
|
|
100
|
-
// in the DOM to select and copy.
|
|
101
|
-
//
|
|
102
|
-
//
|
|
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.
|
|
103
235
|
.cell-text {
|
|
104
236
|
display: block;
|
|
105
|
-
overflow: hidden;
|
|
237
|
+
overflow: var(--spiderly-cell-overflow, hidden);
|
|
106
238
|
text-overflow: ellipsis;
|
|
107
|
-
white-space: nowrap;
|
|
239
|
+
white-space: var(--spiderly-cell-white-space, nowrap);
|
|
108
240
|
}
|
|
109
241
|
|
|
110
242
|
// PrimeNG-rendered markup, so ::ng-deep is the only way in; the mixin carries the contract.
|
|
@@ -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
|
+
}
|
|
@@ -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;
|