current_scope 0.2.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +577 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/current_scope/application.js +225 -0
- data/app/assets/stylesheets/current_scope/application.css +707 -0
- data/app/controllers/current_scope/application_controller.rb +54 -0
- data/app/controllers/current_scope/events_controller.rb +14 -0
- data/app/controllers/current_scope/role_assignments_controller.rb +104 -0
- data/app/controllers/current_scope/roles_controller.rb +135 -0
- data/app/controllers/current_scope/scoped_role_assignments_controller.rb +159 -0
- data/app/controllers/current_scope/subjects_controller.rb +57 -0
- data/app/helpers/current_scope/application_helper.rb +72 -0
- data/app/models/current_scope/application_record.rb +5 -0
- data/app/models/current_scope/current.rb +50 -0
- data/app/models/current_scope/event.rb +117 -0
- data/app/models/current_scope/role.rb +55 -0
- data/app/models/current_scope/role_assignment.rb +21 -0
- data/app/models/current_scope/role_permission.rb +9 -0
- data/app/models/current_scope/scoped_role_assignment.rb +14 -0
- data/app/views/current_scope/events/index.html.erb +41 -0
- data/app/views/current_scope/roles/edit.html.erb +86 -0
- data/app/views/current_scope/roles/index.html.erb +41 -0
- data/app/views/current_scope/roles/members.html.erb +96 -0
- data/app/views/current_scope/roles/new.html.erb +19 -0
- data/app/views/current_scope/scoped_role_assignments/new.html.erb +135 -0
- data/app/views/current_scope/subjects/index.html.erb +100 -0
- data/app/views/layouts/current_scope/application.html.erb +53 -0
- data/config/routes.rb +14 -0
- data/db/migrate/20260710000001_create_current_scope_tables.rb +31 -0
- data/db/migrate/20260710000002_create_current_scope_events.rb +32 -0
- data/db/migrate/20260714000001_add_description_to_current_scope_roles.rb +5 -0
- data/lib/current_scope/configuration.rb +181 -0
- data/lib/current_scope/context.rb +36 -0
- data/lib/current_scope/engine.rb +14 -0
- data/lib/current_scope/gating_tripwire.rb +53 -0
- data/lib/current_scope/guard.rb +97 -0
- data/lib/current_scope/mutation_guard.rb +55 -0
- data/lib/current_scope/permission_catalog.rb +33 -0
- data/lib/current_scope/permission_grid.rb +90 -0
- data/lib/current_scope/permissions.rb +57 -0
- data/lib/current_scope/resolver.rb +171 -0
- data/lib/current_scope/scopeable.rb +38 -0
- data/lib/current_scope/test_helpers.rb +53 -0
- data/lib/current_scope/version.rb +3 -0
- data/lib/current_scope.rb +168 -0
- data/lib/generators/current_scope/install/install_generator.rb +33 -0
- data/lib/generators/current_scope/install/templates/initializer.rb +77 -0
- data/lib/tasks/current_scope_tasks.rake +15 -0
- metadata +113 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
/* CurrentScope — the mounted management UI: an authorization console.
|
|
2
|
+
*
|
|
3
|
+
* An opinionated, self-contained admin dashboard: a sidebar shell, a
|
|
4
|
+
* precision-monochrome surface with one cobalt signature accent (the product's
|
|
5
|
+
* "ledger ink"), monospace for the technical layer (permission keys, action
|
|
6
|
+
* columns, event types), and first-class light + dark themes. System stack
|
|
7
|
+
* only — no web fonts, no build step, CSP-safe.
|
|
8
|
+
*
|
|
9
|
+
* Theming: tokens live on :root. Light is the default; system dark applies via
|
|
10
|
+
* prefers-color-scheme; an explicit choice is set as html[data-cs-theme] (from
|
|
11
|
+
* the current_scope_theme cookie, so there's no flash). Retheme by redefining
|
|
12
|
+
* the --cs-* custom properties; restructure by overriding the .cs-* classes.
|
|
13
|
+
*
|
|
14
|
+
* The token defaults sit in a @layer so a host's own (unlayered) :root override
|
|
15
|
+
* always wins — otherwise the data-cs-theme attribute selectors below would
|
|
16
|
+
* outweigh a plain :root { --cs-accent: … } in the host stylesheet.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
@layer current_scope.tokens {
|
|
20
|
+
:root {
|
|
21
|
+
color-scheme: light;
|
|
22
|
+
|
|
23
|
+
/* Surfaces — cool near-neutral, ink on paper. */
|
|
24
|
+
--cs-bg: oklch(0.992 0.002 264);
|
|
25
|
+
--cs-surface: oklch(0.975 0.003 264);
|
|
26
|
+
--cs-surface-2: oklch(0.955 0.004 264);
|
|
27
|
+
--cs-line: oklch(0.905 0.005 264);
|
|
28
|
+
--cs-line-strong: oklch(0.80 0.008 264);
|
|
29
|
+
|
|
30
|
+
/* Ink. */
|
|
31
|
+
--cs-ink: oklch(0.26 0.014 264);
|
|
32
|
+
--cs-ink-2: oklch(0.18 0.02 264); /* headings */
|
|
33
|
+
--cs-muted: oklch(0.475 0.015 264); /* ~5.4:1 on --cs-bg */
|
|
34
|
+
|
|
35
|
+
/* Cobalt signature accent — used with discipline (active nav, primary
|
|
36
|
+
action, focus, granted state). Rare on purpose. */
|
|
37
|
+
--cs-accent: oklch(0.515 0.163 262);
|
|
38
|
+
--cs-accent-strong: oklch(0.44 0.17 262);
|
|
39
|
+
--cs-accent-wash: oklch(0.955 0.03 262);
|
|
40
|
+
--cs-on-accent: oklch(0.99 0.01 262);
|
|
41
|
+
|
|
42
|
+
/* Semantics. */
|
|
43
|
+
--cs-danger: oklch(0.53 0.19 25);
|
|
44
|
+
--cs-danger-line: oklch(0.82 0.09 25);
|
|
45
|
+
--cs-danger-wash: oklch(0.965 0.018 25);
|
|
46
|
+
--cs-ok: oklch(0.58 0.12 150);
|
|
47
|
+
--cs-ok-line: oklch(0.82 0.07 150);
|
|
48
|
+
--cs-ok-wash: oklch(0.962 0.02 150);
|
|
49
|
+
|
|
50
|
+
--cs-radius: 6px;
|
|
51
|
+
--cs-radius-sm: 4px;
|
|
52
|
+
--cs-shadow: 0 1px 2px oklch(0.2 0.03 264 / 0.06), 0 4px 12px oklch(0.2 0.03 264 / 0.05);
|
|
53
|
+
--cs-font: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
|
|
54
|
+
--cs-mono: ui-monospace, "SF Mono", "SFMono-Regular", Menlo, Consolas, monospace;
|
|
55
|
+
--cs-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
56
|
+
|
|
57
|
+
--cs-sidebar-w: 15.5rem;
|
|
58
|
+
--cs-topbar-h: 3.25rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Dark tokens, shared by system-dark (no explicit choice) and an explicit
|
|
62
|
+
data-cs-theme="dark". */
|
|
63
|
+
@media (prefers-color-scheme: dark) {
|
|
64
|
+
:root:not([data-cs-theme]) {
|
|
65
|
+
color-scheme: dark;
|
|
66
|
+
--cs-bg: oklch(0.172 0.008 264);
|
|
67
|
+
--cs-surface: oklch(0.208 0.009 264);
|
|
68
|
+
--cs-surface-2: oklch(0.238 0.010 264);
|
|
69
|
+
--cs-line: oklch(0.305 0.010 264);
|
|
70
|
+
--cs-line-strong: oklch(0.41 0.013 264);
|
|
71
|
+
--cs-ink: oklch(0.905 0.008 264);
|
|
72
|
+
--cs-ink-2: oklch(0.975 0.006 264);
|
|
73
|
+
--cs-muted: oklch(0.68 0.013 264);
|
|
74
|
+
--cs-accent: oklch(0.70 0.15 262);
|
|
75
|
+
--cs-accent-strong: oklch(0.78 0.14 262);
|
|
76
|
+
--cs-accent-wash: oklch(0.30 0.07 262);
|
|
77
|
+
--cs-on-accent: oklch(0.16 0.03 262);
|
|
78
|
+
--cs-danger: oklch(0.68 0.17 25);
|
|
79
|
+
--cs-danger-line: oklch(0.42 0.11 25);
|
|
80
|
+
--cs-danger-wash: oklch(0.27 0.06 25);
|
|
81
|
+
--cs-ok: oklch(0.72 0.13 150);
|
|
82
|
+
--cs-ok-line: oklch(0.40 0.08 150);
|
|
83
|
+
--cs-ok-wash: oklch(0.26 0.05 150);
|
|
84
|
+
--cs-shadow: 0 1px 2px oklch(0 0 0 / 0.4), 0 6px 16px oklch(0 0 0 / 0.35);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* NB: these dark tokens intentionally duplicate the @media block above — CSS
|
|
89
|
+
can't share a declaration block between a media query and an attribute
|
|
90
|
+
selector without a preprocessor. Keep the two in sync on any token change. */
|
|
91
|
+
:root[data-cs-theme="dark"] {
|
|
92
|
+
color-scheme: dark;
|
|
93
|
+
--cs-bg: oklch(0.172 0.008 264);
|
|
94
|
+
--cs-surface: oklch(0.208 0.009 264);
|
|
95
|
+
--cs-surface-2: oklch(0.238 0.010 264);
|
|
96
|
+
--cs-line: oklch(0.305 0.010 264);
|
|
97
|
+
--cs-line-strong: oklch(0.41 0.013 264);
|
|
98
|
+
--cs-ink: oklch(0.905 0.008 264);
|
|
99
|
+
--cs-ink-2: oklch(0.975 0.006 264);
|
|
100
|
+
--cs-muted: oklch(0.68 0.013 264);
|
|
101
|
+
--cs-accent: oklch(0.70 0.15 262);
|
|
102
|
+
--cs-accent-strong: oklch(0.78 0.14 262);
|
|
103
|
+
--cs-accent-wash: oklch(0.30 0.07 262);
|
|
104
|
+
--cs-on-accent: oklch(0.16 0.03 262);
|
|
105
|
+
--cs-danger: oklch(0.68 0.17 25);
|
|
106
|
+
--cs-danger-line: oklch(0.42 0.11 25);
|
|
107
|
+
--cs-danger-wash: oklch(0.27 0.06 25);
|
|
108
|
+
--cs-ok: oklch(0.72 0.13 150);
|
|
109
|
+
--cs-ok-line: oklch(0.40 0.08 150);
|
|
110
|
+
--cs-ok-wash: oklch(0.26 0.05 150);
|
|
111
|
+
--cs-shadow: 0 1px 2px oklch(0 0 0 / 0.4), 0 6px 16px oklch(0 0 0 / 0.35);
|
|
112
|
+
}
|
|
113
|
+
} /* @layer current_scope.tokens */
|
|
114
|
+
|
|
115
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
116
|
+
|
|
117
|
+
body {
|
|
118
|
+
margin: 0;
|
|
119
|
+
background: var(--cs-bg);
|
|
120
|
+
color: var(--cs-ink);
|
|
121
|
+
font-family: var(--cs-font);
|
|
122
|
+
font-size: 0.9375rem;
|
|
123
|
+
line-height: 1.55;
|
|
124
|
+
-webkit-font-smoothing: antialiased;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
h1 { font-size: 1.4rem; font-weight: 660; letter-spacing: -0.02em; color: var(--cs-ink-2); margin: 0 0 0.375rem; text-wrap: balance; }
|
|
128
|
+
h2 { font-size: 1.0625rem; font-weight: 600; color: var(--cs-ink-2); margin: 2rem 0 0.75rem; text-wrap: balance; }
|
|
129
|
+
|
|
130
|
+
a { color: var(--cs-accent); text-decoration-thickness: 1px; text-underline-offset: 3px; }
|
|
131
|
+
a:hover { color: var(--cs-accent-strong); }
|
|
132
|
+
|
|
133
|
+
:focus-visible { outline: 2px solid var(--cs-accent); outline-offset: 2px; border-radius: 3px; }
|
|
134
|
+
|
|
135
|
+
/* ---- Shell ------------------------------------------------------------ *
|
|
136
|
+
* A left sidebar (wordmark + section nav) and a scrollable main column with
|
|
137
|
+
* a sticky topbar. The dense grid lives inside main.
|
|
138
|
+
*/
|
|
139
|
+
|
|
140
|
+
.cs-shell {
|
|
141
|
+
display: grid;
|
|
142
|
+
grid-template-columns: var(--cs-sidebar-w) minmax(0, 1fr);
|
|
143
|
+
min-height: 100vh;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.cs-sidebar {
|
|
147
|
+
position: sticky;
|
|
148
|
+
top: 0;
|
|
149
|
+
align-self: start;
|
|
150
|
+
height: 100vh;
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
gap: 0.25rem;
|
|
154
|
+
padding: 1.125rem 0.75rem;
|
|
155
|
+
background: var(--cs-surface);
|
|
156
|
+
border-right: 1px solid var(--cs-line);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.cs-brand {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
gap: 0.5rem;
|
|
163
|
+
padding: 0.25rem 0.5rem 1rem;
|
|
164
|
+
font-weight: 660;
|
|
165
|
+
font-size: 0.95rem;
|
|
166
|
+
letter-spacing: -0.01em;
|
|
167
|
+
color: var(--cs-ink-2);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* A tiny 2x2 "grid" mark — the permission matrix, distilled. */
|
|
171
|
+
.cs-brand-mark {
|
|
172
|
+
width: 1.125rem;
|
|
173
|
+
height: 1.125rem;
|
|
174
|
+
border-radius: 4px;
|
|
175
|
+
flex: none;
|
|
176
|
+
background:
|
|
177
|
+
linear-gradient(var(--cs-accent) 0 0) 0 0 / 45% 45%,
|
|
178
|
+
linear-gradient(var(--cs-accent) 0 0) 100% 0 / 45% 45%,
|
|
179
|
+
linear-gradient(var(--cs-line-strong) 0 0) 0 100% / 45% 45%,
|
|
180
|
+
linear-gradient(var(--cs-accent) 0 0) 100% 100% / 45% 45%;
|
|
181
|
+
background-repeat: no-repeat;
|
|
182
|
+
box-shadow: inset 0 0 0 1px var(--cs-line);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.cs-nav { display: flex; flex-direction: column; gap: 0.125rem; }
|
|
186
|
+
|
|
187
|
+
.cs-nav a {
|
|
188
|
+
position: relative;
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: 0.5rem;
|
|
192
|
+
padding: 0.4375rem 0.625rem;
|
|
193
|
+
border-radius: var(--cs-radius-sm);
|
|
194
|
+
color: var(--cs-muted);
|
|
195
|
+
text-decoration: none;
|
|
196
|
+
font-weight: 500;
|
|
197
|
+
font-size: 0.875rem;
|
|
198
|
+
transition: background 120ms var(--cs-ease), color 120ms var(--cs-ease);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.cs-nav a:hover { color: var(--cs-ink); background: var(--cs-surface-2); }
|
|
202
|
+
|
|
203
|
+
.cs-nav a[aria-current="page"] {
|
|
204
|
+
color: var(--cs-ink-2);
|
|
205
|
+
background: var(--cs-accent-wash);
|
|
206
|
+
font-weight: 560;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* the signature accent rail on the active item */
|
|
210
|
+
.cs-nav a[aria-current="page"]::before {
|
|
211
|
+
content: "";
|
|
212
|
+
position: absolute;
|
|
213
|
+
left: -0.75rem;
|
|
214
|
+
top: 0.375rem;
|
|
215
|
+
bottom: 0.375rem;
|
|
216
|
+
width: 3px;
|
|
217
|
+
border-radius: 0 3px 3px 0;
|
|
218
|
+
background: var(--cs-accent);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.cs-sidebar-foot { margin-top: auto; padding: 0.5rem; color: var(--cs-muted); font-size: 0.75rem; }
|
|
222
|
+
|
|
223
|
+
/* ---- Topbar ----------------------------------------------------------- */
|
|
224
|
+
|
|
225
|
+
.cs-topbar {
|
|
226
|
+
position: sticky;
|
|
227
|
+
top: 0;
|
|
228
|
+
z-index: 5;
|
|
229
|
+
display: flex;
|
|
230
|
+
align-items: center;
|
|
231
|
+
min-height: var(--cs-topbar-h);
|
|
232
|
+
gap: 1rem;
|
|
233
|
+
padding: 0.75rem 1.75rem;
|
|
234
|
+
background: color-mix(in oklch, var(--cs-bg) 82%, transparent);
|
|
235
|
+
backdrop-filter: saturate(1.4) blur(8px);
|
|
236
|
+
border-bottom: 1px solid var(--cs-line);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.cs-topbar-title { font-weight: 600; font-size: 0.9375rem; color: var(--cs-ink-2); }
|
|
240
|
+
.cs-topbar-spacer { flex: 1; }
|
|
241
|
+
|
|
242
|
+
.cs-badge {
|
|
243
|
+
display: inline-flex;
|
|
244
|
+
align-items: center;
|
|
245
|
+
gap: 0.375rem;
|
|
246
|
+
padding: 0.1875rem 0.5rem;
|
|
247
|
+
border: 1px solid var(--cs-line-strong);
|
|
248
|
+
border-radius: 999px;
|
|
249
|
+
font-size: 0.75rem;
|
|
250
|
+
color: var(--cs-muted);
|
|
251
|
+
font-family: var(--cs-mono);
|
|
252
|
+
}
|
|
253
|
+
.cs-badge::before { content: ""; width: 6px; height: 6px; border-radius: 999px; background: var(--cs-ok); }
|
|
254
|
+
|
|
255
|
+
/* Theme toggle — sun/moon glyphs swapped by theme via CSS. */
|
|
256
|
+
.cs-theme-toggle {
|
|
257
|
+
display: inline-grid;
|
|
258
|
+
place-items: center;
|
|
259
|
+
width: 2rem;
|
|
260
|
+
height: 2rem;
|
|
261
|
+
padding: 0;
|
|
262
|
+
border: 1px solid var(--cs-line-strong);
|
|
263
|
+
border-radius: var(--cs-radius-sm);
|
|
264
|
+
background: var(--cs-surface);
|
|
265
|
+
color: var(--cs-ink);
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
font-size: 0.95rem;
|
|
268
|
+
line-height: 1;
|
|
269
|
+
}
|
|
270
|
+
.cs-theme-toggle:hover { border-color: var(--cs-ink); background: var(--cs-surface-2); }
|
|
271
|
+
.cs-theme-toggle .cs-icon-dark { display: none; }
|
|
272
|
+
.cs-theme-toggle .cs-icon-light { display: block; }
|
|
273
|
+
:root[data-cs-theme="dark"] .cs-theme-toggle .cs-icon-dark { display: block; }
|
|
274
|
+
:root[data-cs-theme="dark"] .cs-theme-toggle .cs-icon-light { display: none; }
|
|
275
|
+
@media (prefers-color-scheme: dark) {
|
|
276
|
+
:root:not([data-cs-theme]) .cs-theme-toggle .cs-icon-dark { display: block; }
|
|
277
|
+
:root:not([data-cs-theme]) .cs-theme-toggle .cs-icon-light { display: none; }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/* ---- Main + flashes --------------------------------------------------- */
|
|
281
|
+
|
|
282
|
+
.cs-main { min-width: 0; }
|
|
283
|
+
.cs-content { padding: 1.75rem; max-width: 74rem; }
|
|
284
|
+
.cs-lede { color: var(--cs-muted); font-size: 0.875rem; margin: 0 0 1.5rem; max-width: 64ch; }
|
|
285
|
+
|
|
286
|
+
.cs-flash {
|
|
287
|
+
display: flex;
|
|
288
|
+
gap: 0.5rem;
|
|
289
|
+
margin: 1rem 1.75rem -0.5rem;
|
|
290
|
+
padding: 0.5rem 0.875rem;
|
|
291
|
+
border: 1px solid var(--cs-line);
|
|
292
|
+
border-radius: var(--cs-radius);
|
|
293
|
+
font-size: 0.875rem;
|
|
294
|
+
}
|
|
295
|
+
.cs-flash--notice { border-color: var(--cs-ok-line); background: var(--cs-ok-wash); }
|
|
296
|
+
.cs-flash--alert { border-color: var(--cs-danger-line); background: var(--cs-danger-wash); }
|
|
297
|
+
|
|
298
|
+
/* In-view flash aliases (form errors use these). */
|
|
299
|
+
.cs-notice, .cs-alert {
|
|
300
|
+
display: block;
|
|
301
|
+
padding: 0.5rem 0.875rem;
|
|
302
|
+
border: 1px solid var(--cs-line);
|
|
303
|
+
border-radius: var(--cs-radius);
|
|
304
|
+
font-size: 0.875rem;
|
|
305
|
+
margin: 0 0 1rem;
|
|
306
|
+
max-width: 30rem;
|
|
307
|
+
}
|
|
308
|
+
.cs-notice { border-color: var(--cs-ok-line); background: var(--cs-ok-wash); }
|
|
309
|
+
.cs-alert { border-color: var(--cs-danger-line); background: var(--cs-danger-wash); }
|
|
310
|
+
|
|
311
|
+
.cs-hint { color: var(--cs-muted); font-size: 0.8125rem; max-width: 68ch; }
|
|
312
|
+
.cs-subtle { color: var(--cs-muted); font-size: 0.8125rem; margin-top: 0.125rem; }
|
|
313
|
+
|
|
314
|
+
/* ---- Toolbar (page actions) ------------------------------------------- */
|
|
315
|
+
|
|
316
|
+
.cs-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 0.75rem; margin: 0 0 1.25rem; }
|
|
317
|
+
.cs-toolbar .cs-spacer { flex: 1; }
|
|
318
|
+
|
|
319
|
+
/* ---- Filter + bulk (subjects) ----------------------------------------- */
|
|
320
|
+
|
|
321
|
+
.cs-filter { min-width: min(22rem, 100%); }
|
|
322
|
+
|
|
323
|
+
/* The subjects search form: input + Search + Clear on one line, the input
|
|
324
|
+
taking the free space; wraps on a narrow toolbar. */
|
|
325
|
+
.cs-search { display: flex; align-items: center; gap: 0.5rem; flex: 1 1 22rem; min-width: 0; }
|
|
326
|
+
.cs-search .cs-filter { flex: 1; min-width: 0; }
|
|
327
|
+
|
|
328
|
+
.cs-bulk {
|
|
329
|
+
display: flex;
|
|
330
|
+
align-items: center;
|
|
331
|
+
gap: 0.75rem;
|
|
332
|
+
margin: 0 0 1rem;
|
|
333
|
+
padding: 0.5rem 0.875rem;
|
|
334
|
+
border: 1px solid var(--cs-accent);
|
|
335
|
+
border-radius: var(--cs-radius);
|
|
336
|
+
background: var(--cs-accent-wash);
|
|
337
|
+
font-size: 0.875rem;
|
|
338
|
+
}
|
|
339
|
+
/* The bar sets display:flex, which otherwise defeats the [hidden] attribute the
|
|
340
|
+
JS toggles — keep it hidden until a subject is selected. */
|
|
341
|
+
.cs-bulk[hidden] { display: none; }
|
|
342
|
+
.cs-bulk .cs-spacer { flex: 1; }
|
|
343
|
+
.cs-bulk-org { display: inline-flex; align-items: center; gap: 0.375rem; margin: 0; }
|
|
344
|
+
.cs-bulk-org select { padding: 0.3125rem 0.5rem; }
|
|
345
|
+
|
|
346
|
+
/* Select column — narrow, centered checkboxes. */
|
|
347
|
+
.cs-select-cell { width: 1%; text-align: center; padding-inline: 0.75rem; }
|
|
348
|
+
|
|
349
|
+
.cs-filter-empty { color: var(--cs-muted); font-size: 0.875rem; margin: 0.75rem 0; }
|
|
350
|
+
|
|
351
|
+
/* ---- Cards ------------------------------------------------------------ */
|
|
352
|
+
|
|
353
|
+
.cs-card {
|
|
354
|
+
background: var(--cs-surface);
|
|
355
|
+
border: 1px solid var(--cs-line);
|
|
356
|
+
border-radius: var(--cs-radius);
|
|
357
|
+
box-shadow: var(--cs-shadow);
|
|
358
|
+
}
|
|
359
|
+
.cs-card:not(.cs-card--flush) > * { padding-inline: 1.125rem; }
|
|
360
|
+
/* Flush cards hold data tables. Let them scroll horizontally inside the card so
|
|
361
|
+
a wide table never overflows the page. On a wide screen the table fills the
|
|
362
|
+
card (base width:100%) and scoped-role chips wrap in their own cell — all
|
|
363
|
+
visible, no scroll. On a narrow screen the table switches to its natural width
|
|
364
|
+
(see the max-width:820px block) so columns stay usable and the row height
|
|
365
|
+
stays normal, scrolling horizontally instead of crushing into tall rows. */
|
|
366
|
+
.cs-card--flush { padding: 0; overflow-x: auto; }
|
|
367
|
+
.cs-card--flush .cs-table th:first-child,
|
|
368
|
+
.cs-card--flush .cs-table td:first-child { padding-left: 1.125rem; }
|
|
369
|
+
.cs-card--flush .cs-table th:last-child,
|
|
370
|
+
.cs-card--flush .cs-table td:last-child { padding-right: 1.125rem; }
|
|
371
|
+
|
|
372
|
+
/* ---- Tables ----------------------------------------------------------- */
|
|
373
|
+
|
|
374
|
+
.cs-table { border-collapse: collapse; width: 100%; }
|
|
375
|
+
|
|
376
|
+
.cs-table th {
|
|
377
|
+
text-align: left;
|
|
378
|
+
font-size: 0.7rem;
|
|
379
|
+
font-weight: 600;
|
|
380
|
+
letter-spacing: 0.04em;
|
|
381
|
+
text-transform: uppercase;
|
|
382
|
+
color: var(--cs-muted);
|
|
383
|
+
background: var(--cs-surface-2);
|
|
384
|
+
border-bottom: 1px solid var(--cs-line);
|
|
385
|
+
padding: 0.5rem 0.75rem;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.cs-table td {
|
|
389
|
+
border-bottom: 1px solid var(--cs-line);
|
|
390
|
+
padding: 0.625rem 0.75rem;
|
|
391
|
+
vertical-align: middle;
|
|
392
|
+
}
|
|
393
|
+
.cs-table tbody tr:last-child td { border-bottom: none; }
|
|
394
|
+
.cs-table tbody tr:hover td { background: var(--cs-surface-2); }
|
|
395
|
+
.cs-mono, .cs-table .cs-mono { font-family: var(--cs-mono); font-size: 0.8125rem; }
|
|
396
|
+
|
|
397
|
+
/* ---- Permission grid — the hero surface -------------------------------
|
|
398
|
+
* A true matrix: fixed action columns across the top, one row per controller,
|
|
399
|
+
* a sticky mono controller column down the side. Columns stay ABSOLUTE — a
|
|
400
|
+
* controller that doesn't route a column's actions shows a blank cell, never a
|
|
401
|
+
* shifted one. The signature move is the GRANTED state: a checked cell lights
|
|
402
|
+
* up in the cobalt wash, so access reads at a glance across the whole grid.
|
|
403
|
+
*/
|
|
404
|
+
|
|
405
|
+
.cs-grid-wrap {
|
|
406
|
+
overflow-x: auto;
|
|
407
|
+
border: 1px solid var(--cs-line);
|
|
408
|
+
border-radius: var(--cs-radius);
|
|
409
|
+
box-shadow: var(--cs-shadow);
|
|
410
|
+
background: var(--cs-surface);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.cs-grid { border-collapse: separate; border-spacing: 0; width: 100%; }
|
|
414
|
+
|
|
415
|
+
/* Column headers — the fixed action/group names. Sticky to the TOP OF THE GRID
|
|
416
|
+
WRAPPER (its sticky containing block, because .cs-grid-wrap sets overflow-x) —
|
|
417
|
+
so top:0, NOT the topbar height: any positive offset here pushes the header
|
|
418
|
+
down over the first data row instead of pinning it under the page topbar. */
|
|
419
|
+
.cs-grid thead th {
|
|
420
|
+
position: sticky;
|
|
421
|
+
top: 0;
|
|
422
|
+
z-index: 2;
|
|
423
|
+
background: var(--cs-surface-2);
|
|
424
|
+
color: var(--cs-muted);
|
|
425
|
+
font-family: var(--cs-mono);
|
|
426
|
+
font-size: 0.7rem;
|
|
427
|
+
font-weight: 600;
|
|
428
|
+
letter-spacing: 0.03em;
|
|
429
|
+
text-transform: uppercase;
|
|
430
|
+
text-align: center;
|
|
431
|
+
white-space: nowrap;
|
|
432
|
+
padding: 0.5rem 0.625rem;
|
|
433
|
+
border-bottom: 1px solid var(--cs-line);
|
|
434
|
+
border-right: 1px solid var(--cs-line);
|
|
435
|
+
}
|
|
436
|
+
.cs-grid thead th:last-child { border-right: none; }
|
|
437
|
+
.cs-grid thead th.cs-grid-corner {
|
|
438
|
+
left: 0;
|
|
439
|
+
z-index: 3;
|
|
440
|
+
text-align: left;
|
|
441
|
+
background: var(--cs-surface-2);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/* Sticky controller column. */
|
|
445
|
+
.cs-grid th[scope="row"] {
|
|
446
|
+
position: sticky;
|
|
447
|
+
left: 0;
|
|
448
|
+
z-index: 1;
|
|
449
|
+
background: var(--cs-surface);
|
|
450
|
+
color: var(--cs-ink-2);
|
|
451
|
+
text-align: left;
|
|
452
|
+
white-space: nowrap;
|
|
453
|
+
border-right: 1px solid var(--cs-line);
|
|
454
|
+
border-bottom: 1px solid var(--cs-line);
|
|
455
|
+
padding: 0.375rem 0.75rem;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.cs-grid td {
|
|
459
|
+
border-bottom: 1px solid var(--cs-line);
|
|
460
|
+
border-right: 1px solid var(--cs-line);
|
|
461
|
+
padding: 0.375rem;
|
|
462
|
+
text-align: center;
|
|
463
|
+
transition: background 120ms var(--cs-ease);
|
|
464
|
+
}
|
|
465
|
+
.cs-grid td:last-child { border-right: none; }
|
|
466
|
+
.cs-grid tr:last-child th[scope="row"],
|
|
467
|
+
.cs-grid tr:last-child td { border-bottom: none; }
|
|
468
|
+
.cs-grid tbody tr:hover th[scope="row"] { background: var(--cs-surface-2); }
|
|
469
|
+
.cs-grid tbody tr:hover td:not(.cs-cell-blank):not(:has(input:checked)) { background: var(--cs-surface-2); }
|
|
470
|
+
|
|
471
|
+
.cs-grid label { display: inline-flex; align-items: center; justify-content: center; margin: 0; cursor: pointer; }
|
|
472
|
+
|
|
473
|
+
/* A blank cell — the controller doesn't route this column's actions. A faint
|
|
474
|
+
marker so the gap reads as "not applicable", not "unchecked". */
|
|
475
|
+
.cs-grid td.cs-cell-blank::after { content: "·"; color: var(--cs-line-strong); font-size: 1rem; }
|
|
476
|
+
|
|
477
|
+
/* Per-row master toggle ("enable all in this controller") — sits in the sticky
|
|
478
|
+
row header, reading as a select-all next to the mono controller name. */
|
|
479
|
+
.cs-grid .cs-row-all {
|
|
480
|
+
display: inline-flex;
|
|
481
|
+
align-items: center;
|
|
482
|
+
gap: 0.5rem;
|
|
483
|
+
cursor: pointer;
|
|
484
|
+
font-family: var(--cs-mono);
|
|
485
|
+
font-size: 0.8125rem;
|
|
486
|
+
font-weight: 600;
|
|
487
|
+
color: var(--cs-ink-2);
|
|
488
|
+
}
|
|
489
|
+
.cs-grid .cs-row-all input { flex: none; margin: 0; }
|
|
490
|
+
.cs-grid th[scope="row"]:has([data-cs-row-all]:checked) { background: var(--cs-accent-wash); }
|
|
491
|
+
|
|
492
|
+
/* GRANTED: the cell lights up in cobalt. */
|
|
493
|
+
.cs-grid td:has(input:checked) { background: var(--cs-accent-wash); }
|
|
494
|
+
|
|
495
|
+
/* PARTIAL: some-but-not-all of the group is granted. JS also sets the checkbox
|
|
496
|
+
indeterminate, but this keys off the data attribute so the "partial, not
|
|
497
|
+
ungranted" state is still visible with JS disabled. */
|
|
498
|
+
.cs-grid td:has(input[data-cs-partial="true"]) { background: var(--cs-accent-wash); }
|
|
499
|
+
.cs-grid input[data-cs-partial="true"] { outline: 2px dashed var(--cs-accent); outline-offset: 1px; }
|
|
500
|
+
|
|
501
|
+
/* Checkboxes / radios use the accent. */
|
|
502
|
+
input[type="checkbox"], input[type="radio"] {
|
|
503
|
+
accent-color: var(--cs-accent);
|
|
504
|
+
width: 1rem;
|
|
505
|
+
height: 1rem;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/* ---- Chips (scoped-role assignments) ---------------------------------- */
|
|
509
|
+
|
|
510
|
+
.cs-chip {
|
|
511
|
+
display: inline-flex;
|
|
512
|
+
align-items: stretch;
|
|
513
|
+
border: 1px solid var(--cs-line-strong);
|
|
514
|
+
background: var(--cs-surface);
|
|
515
|
+
border-radius: 999px;
|
|
516
|
+
margin: 0 0.25rem 0.25rem 0;
|
|
517
|
+
font-size: 0.8125rem;
|
|
518
|
+
overflow: hidden;
|
|
519
|
+
}
|
|
520
|
+
.cs-chip-label { padding: 0.125rem 0.25rem 0.125rem 0.6875rem; align-self: center; }
|
|
521
|
+
.cs-chip form { display: flex; margin: 0; }
|
|
522
|
+
|
|
523
|
+
/* Revoke control — a clear, labeled × with a divider so it reads as a button. */
|
|
524
|
+
.cs-chip-remove {
|
|
525
|
+
border: none;
|
|
526
|
+
border-left: 1px solid var(--cs-line);
|
|
527
|
+
border-radius: 0;
|
|
528
|
+
background: none;
|
|
529
|
+
cursor: pointer;
|
|
530
|
+
padding: 0 0.5rem;
|
|
531
|
+
color: var(--cs-muted);
|
|
532
|
+
font-size: 0.95rem;
|
|
533
|
+
line-height: 1;
|
|
534
|
+
transition: background 120ms var(--cs-ease), color 120ms var(--cs-ease);
|
|
535
|
+
}
|
|
536
|
+
.cs-chip-remove:hover, .cs-chip-remove:focus-visible {
|
|
537
|
+
color: var(--cs-on-accent);
|
|
538
|
+
background: var(--cs-danger);
|
|
539
|
+
border-color: var(--cs-danger);
|
|
540
|
+
outline: none;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/* "+ scoped role" reads as an add affordance. */
|
|
544
|
+
.cs-add-scoped {
|
|
545
|
+
display: inline-flex;
|
|
546
|
+
align-items: center;
|
|
547
|
+
gap: 0.25rem;
|
|
548
|
+
font-size: 0.8125rem;
|
|
549
|
+
text-decoration: none;
|
|
550
|
+
color: var(--cs-accent);
|
|
551
|
+
padding: 0.125rem 0.4375rem;
|
|
552
|
+
border: 1px dashed var(--cs-line-strong);
|
|
553
|
+
border-radius: 999px;
|
|
554
|
+
}
|
|
555
|
+
.cs-add-scoped:hover { border-color: var(--cs-accent); background: var(--cs-accent-wash); }
|
|
556
|
+
|
|
557
|
+
/* ---- Forms ------------------------------------------------------------ */
|
|
558
|
+
|
|
559
|
+
/* Scoped to the content region: these layout rules must not reach a form the
|
|
560
|
+
host might place in the sidebar or topbar. */
|
|
561
|
+
.cs-main form p { margin: 0 0 1rem; max-width: 30rem; }
|
|
562
|
+
|
|
563
|
+
.cs-main form p > label {
|
|
564
|
+
display: block;
|
|
565
|
+
font-size: 0.7rem;
|
|
566
|
+
font-weight: 600;
|
|
567
|
+
letter-spacing: 0.03em;
|
|
568
|
+
text-transform: uppercase;
|
|
569
|
+
color: var(--cs-muted);
|
|
570
|
+
margin-bottom: 0.375rem;
|
|
571
|
+
}
|
|
572
|
+
.cs-main form p > label:has(input) {
|
|
573
|
+
display: flex;
|
|
574
|
+
align-items: center;
|
|
575
|
+
gap: 0.4375rem;
|
|
576
|
+
font-size: 0.875rem;
|
|
577
|
+
font-weight: 500;
|
|
578
|
+
letter-spacing: 0;
|
|
579
|
+
text-transform: none;
|
|
580
|
+
color: var(--cs-ink);
|
|
581
|
+
cursor: pointer;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
input[type="text"], input[type="email"], input[type="password"],
|
|
585
|
+
input[type="search"], select, textarea {
|
|
586
|
+
font: inherit;
|
|
587
|
+
color: var(--cs-ink);
|
|
588
|
+
background: var(--cs-bg);
|
|
589
|
+
border: 1px solid var(--cs-line-strong);
|
|
590
|
+
border-radius: var(--cs-radius-sm);
|
|
591
|
+
padding: 0.4375rem 0.625rem;
|
|
592
|
+
max-width: 100%;
|
|
593
|
+
}
|
|
594
|
+
.cs-main form p > input[type="text"],
|
|
595
|
+
.cs-main form p > input[type="email"],
|
|
596
|
+
.cs-main form p > input[type="password"],
|
|
597
|
+
.cs-main form p > select,
|
|
598
|
+
.cs-main form p > textarea { width: 100%; }
|
|
599
|
+
textarea { resize: vertical; min-height: 2.5rem; }
|
|
600
|
+
|
|
601
|
+
input::placeholder { color: var(--cs-muted); opacity: 1; }
|
|
602
|
+
input:focus-visible, select:focus-visible, textarea:focus-visible {
|
|
603
|
+
outline: 2px solid var(--cs-accent);
|
|
604
|
+
outline-offset: 0;
|
|
605
|
+
border-color: var(--cs-accent);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/* ---- Buttons ---------------------------------------------------------- */
|
|
609
|
+
|
|
610
|
+
button, input[type="submit"], .cs-btn {
|
|
611
|
+
appearance: none;
|
|
612
|
+
display: inline-flex;
|
|
613
|
+
align-items: center;
|
|
614
|
+
gap: 0.375rem;
|
|
615
|
+
font: 500 0.875rem var(--cs-font);
|
|
616
|
+
color: var(--cs-ink);
|
|
617
|
+
background: var(--cs-surface);
|
|
618
|
+
border: 1px solid var(--cs-line-strong);
|
|
619
|
+
border-radius: var(--cs-radius-sm);
|
|
620
|
+
padding: 0.4375rem 0.875rem;
|
|
621
|
+
cursor: pointer;
|
|
622
|
+
text-decoration: none;
|
|
623
|
+
transition: background 130ms var(--cs-ease), border-color 130ms var(--cs-ease), translate 90ms var(--cs-ease);
|
|
624
|
+
}
|
|
625
|
+
.cs-btn:hover { color: var(--cs-ink); }
|
|
626
|
+
button:hover, input[type="submit"]:hover, .cs-btn:hover { background: var(--cs-surface-2); border-color: var(--cs-ink); }
|
|
627
|
+
button:active, input[type="submit"]:active, .cs-btn:active { translate: 0 1px; }
|
|
628
|
+
button:disabled, input[type="submit"]:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
629
|
+
|
|
630
|
+
/* Primary — the one committed action per form. */
|
|
631
|
+
.cs-btn-primary,
|
|
632
|
+
form > p:last-child input[type="submit"] {
|
|
633
|
+
color: var(--cs-on-accent);
|
|
634
|
+
background: var(--cs-accent);
|
|
635
|
+
border-color: var(--cs-accent);
|
|
636
|
+
}
|
|
637
|
+
.cs-btn-primary:hover,
|
|
638
|
+
form > p:last-child input[type="submit"]:hover {
|
|
639
|
+
background: var(--cs-accent-strong);
|
|
640
|
+
border-color: var(--cs-accent-strong);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.cs-btn-danger { color: var(--cs-danger); border-color: var(--cs-danger-line); }
|
|
644
|
+
.cs-btn-danger:hover { color: var(--cs-on-accent); background: var(--cs-danger); border-color: var(--cs-danger); }
|
|
645
|
+
|
|
646
|
+
/* Inline row actions shouldn't shout. */
|
|
647
|
+
.cs-table form { display: inline; margin: 0; }
|
|
648
|
+
|
|
649
|
+
/* ---- Scoped-role picker ----------------------------------------------- */
|
|
650
|
+
|
|
651
|
+
.cs-picker turbo-frame { display: block; }
|
|
652
|
+
.cs-picker-empty { max-width: 30rem; margin: 0 0 1rem; }
|
|
653
|
+
.cs-picker-empty code {
|
|
654
|
+
font-family: var(--cs-mono);
|
|
655
|
+
font-size: 0.8125rem;
|
|
656
|
+
background: var(--cs-surface-2);
|
|
657
|
+
border: 1px solid var(--cs-line);
|
|
658
|
+
border-radius: 3px;
|
|
659
|
+
padding: 0.0625rem 0.25rem;
|
|
660
|
+
}
|
|
661
|
+
.cs-picker-actions { display: flex; align-items: center; gap: 0.625rem; }
|
|
662
|
+
.cs-picker-refresh { font-size: 0.8125rem; color: var(--cs-muted); }
|
|
663
|
+
|
|
664
|
+
/* ---- Pagination ------------------------------------------------------- */
|
|
665
|
+
|
|
666
|
+
.cs-pagination {
|
|
667
|
+
display: flex;
|
|
668
|
+
align-items: center;
|
|
669
|
+
gap: 1rem;
|
|
670
|
+
margin-top: 1rem;
|
|
671
|
+
font-size: 0.8125rem;
|
|
672
|
+
color: var(--cs-muted);
|
|
673
|
+
}
|
|
674
|
+
.cs-pagination a { text-decoration: none; }
|
|
675
|
+
|
|
676
|
+
/* ---- Motion ----------------------------------------------------------- */
|
|
677
|
+
|
|
678
|
+
@media (prefers-reduced-motion: reduce) {
|
|
679
|
+
*, *::before, *::after { transition-duration: 0.01ms !important; }
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/* ---- Responsive ------------------------------------------------------- *
|
|
683
|
+
* Collapse the sidebar to a top strip on narrow viewports.
|
|
684
|
+
*/
|
|
685
|
+
|
|
686
|
+
@media (max-width: 820px) {
|
|
687
|
+
.cs-shell { grid-template-columns: 1fr; }
|
|
688
|
+
.cs-sidebar {
|
|
689
|
+
position: static;
|
|
690
|
+
height: auto;
|
|
691
|
+
flex-direction: row;
|
|
692
|
+
align-items: center;
|
|
693
|
+
flex-wrap: wrap;
|
|
694
|
+
gap: 0.25rem 0.5rem;
|
|
695
|
+
border-right: none;
|
|
696
|
+
border-bottom: 1px solid var(--cs-line);
|
|
697
|
+
}
|
|
698
|
+
.cs-brand { padding: 0.25rem 0.5rem; }
|
|
699
|
+
.cs-nav { flex-direction: row; flex-wrap: wrap; }
|
|
700
|
+
.cs-nav a[aria-current="page"]::before { display: none; }
|
|
701
|
+
.cs-sidebar-foot { display: none; }
|
|
702
|
+
.cs-content { padding: 1.25rem; }
|
|
703
|
+
.cs-topbar { padding: 0.75rem 1.25rem; }
|
|
704
|
+
/* Data tables take their natural width and scroll inside the flush card,
|
|
705
|
+
rather than shrinking columns until cells wrap into very tall rows. */
|
|
706
|
+
.cs-card--flush .cs-table { width: max-content; }
|
|
707
|
+
}
|