rustlavel-cli 0.7.4

The rustlavel command-line tool: new, serve, make:*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
/* The starter kit's stylesheet.
 *
 * Built with Tailwind v4 and committed as `public/css/app.css`, so a project
 * needs no Node toolchain to run. Rebuild after editing this file, or after
 * using a Tailwind class that no page used before:
 *
 *     npx @tailwindcss/cli -i resources/css/app.css -o public/css/app.css --minify
 *
 * Everything is in this file and in `public/js/app.js` on purpose. The pages
 * are served under a Content-Security-Policy with no `unsafe-inline`, so a
 * `<style>` block or an `onclick=` attribute anywhere would simply not run —
 * and a policy that has to be loosened for the framework's own markup is a
 * policy that protects nothing.
 */

/* `source(none)` turns off Tailwind's automatic scanning and the two `@source`
 * lines say exactly where to look instead. Left to itself it would walk up to
 * the repository root and scan everything, which is both slow and wrong: this
 * stylesheet serves these templates and nothing else. */
@import "tailwindcss" source(none);

@source "../views/**/*.rl.html";
/* The password meter picks its colour and width classes at runtime, so they
 * appear only here and would otherwise be swept out as unused. */
@source "../../public/js/app.js";
/* And the controllers. A statistic's tint and an icon button's colour are
 * chosen in Rust, because the choice belongs with the number it describes —
 * which means those class names appear nowhere in a template and would
 * otherwise be swept out as unused. */
@source "../../src/**/*.rs";

/* Dark mode is a class on <html>, not a media query, because the preference is
 * a cookie the server reads: it renders `class="dark"` on the first response.
 * A toggle done in JavaScript would flash the wrong theme before it ran, and
 * the usual fix — a tiny inline script in <head> — is exactly what the CSP
 * forbids. */
@custom-variant dark (&:where(.dark, .dark *));

/* Inter, self-hosted.
 *
 * The face the pages are drawn in, served from this application rather than
 * from a font CDN: a starter kit that reaches out to fonts.gstatic.com breaks
 * behind a firewall, leaks a request per visitor to a third party, and needs
 * the Content-Security-Policy this kit is written against opened up. The files
 * are in `public/fonts/`, under the SIL Open Font License that sits beside
 * them.
 *
 * One variable file per subset, and `unicode-range` is what makes that worth
 * doing: a page with no accented characters never asks for the Latin Extended
 * half. `swap` because text a person cannot read yet is worse than text in the
 * wrong face for 100ms.
 */
@font-face {
  font-family: Inter;
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/inter-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: Inter;
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/inter-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@theme {
  /* The neutrals are a blue-grey, not a pure grey.
   *
   * Taken from the operations dashboard this kit's look comes from: every
   * surface, border and label there sits on a hue of about 250, which is what
   * keeps a page of tables and forms from reading as photocopier grey. The
   * numbering keeps its meaning — 200 and 300 are borders, 400 and 500 are
   * quiet text, 700 is a label, 900 is body copy — so a template written
   * against the old scale still says what it meant. */
  --color-ink-50:  oklch(0.971 0.007 248);  /* page ground   #F2F6FA */
  --color-ink-100: oklch(0.958 0.009 248);  /* raised ground #EDF2F7 */
  --color-ink-200: oklch(0.934 0.013 251);  /* card border   #E3EAF2 */
  --color-ink-300: oklch(0.909 0.017 251);  /* input border  #D9E2EC */
  --color-ink-400: oklch(0.699 0.041 247);  /* placeholder   #8AA1B7 */
  --color-ink-500: oklch(0.554 0.041 257);  /* muted text    #64748B */
  --color-ink-600: oklch(0.495 0.059 248);  /*               #476582 */
  --color-ink-700: oklch(0.414 0.055 248);  /* field label   #334E68 */
  --color-ink-800: oklch(0.296 0.072 251);  /* sidebar navy  #0B2E4F */
  --color-ink-900: oklch(0.248 0.065 251);  /* body copy            */
  --color-ink-950: oklch(0.190 0.055 251);  /* dark ground          */

  /* The eleven brand steps for #0E5FA8.
   *
   * Not typed by hand: these are exactly what `support::palette` computes for
   * that colour at runtime, so the stylesheet's default and whatever Settings
   * → Appearance generates are the same ramp rather than two that drift. Change
   * the default in `support::settings` and regenerate these from it. */
  --color-brand-50:  oklch(0.970 0.009 252.0);
  --color-brand-100: oklch(0.932 0.020 252.0);
  --color-brand-200: oklch(0.882 0.038 252.0);
  --color-brand-300: oklch(0.809 0.067 252.0);
  --color-brand-400: oklch(0.707 0.106 252.0);
  --color-brand-500: oklch(0.623 0.137 252.0);
  --color-brand-600: oklch(0.546 0.157 252.0);
  --color-brand-700: oklch(0.488 0.156 252.0);
  --color-brand-800: oklch(0.424 0.127 252.0);
  --color-brand-900: oklch(0.379 0.093 252.0);
  --color-brand-950: oklch(0.282 0.070 252.0);

  /* Inter where it is installed, and the platform's own face where it is not.
   * No web font is downloaded: a starter kit that fetches from a font CDN
   * either breaks behind a firewall or needs the Content-Security-Policy
   * opened up, and neither is worth a slightly different `g`. */
  --font-sans: Inter, ui-sans-serif, system-ui, -apple-system,
               "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-mono, ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

  /* Denser than Tailwind's default, which is what a screen of tables wants:
   * 13px body, 12px for the second line under it. */
  --text-sm: 0.8125rem;
  --text-sm--line-height: 1.25rem;

  /* 10px on a control, 12px on a card — the two radii the design uses. */
  --radius-lg: 0.625rem;
  --radius-card: 0.75rem;
}

/* A handful of component classes, so a page reads as what it is rather than as
 * twelve utilities in a row. Anything used once stays inline; anything a form
 * repeats on every page earns a name. */
/* The component classes.
 *
 * Every one of these is an `@utility` rather than a rule in `@layer
 * components`, because in Tailwind v4 only a real utility may appear inside
 * an `@apply` — and these build on each other. A plain component-layer rule
 * would work when written in markup and fail the moment another rule tried to
 * compose it, which is a confusing way to find out.
 */

/* Tick boxes in the brand colour rather than the browser's grey square. One
 * declaration, and it covers every checkbox and radio in the kit. */
@layer base {
  input[type="checkbox"],
  input[type="radio"] {
    accent-color: var(--color-brand-700);
    width: 0.9375rem;
    height: 0.9375rem;
  }
}

@utility field-label {
  @apply mb-1.5 block text-xs font-bold text-ink-700 dark:text-ink-300;
}

@utility field-input {
  @apply block w-full rounded-lg border border-ink-300 bg-white px-3.5 py-2.5 text-sm
         text-ink-900 transition
         placeholder:text-ink-400
         focus:border-brand-500 focus:outline-none focus:ring-2 focus:ring-brand-500/30
         disabled:cursor-not-allowed disabled:bg-ink-100
         dark:border-ink-700 dark:bg-ink-900 dark:text-ink-100 dark:placeholder:text-ink-500
         dark:disabled:bg-ink-800;
}

/* The one field that sits in the header rather than in a form: tinted at rest
 * so it reads as a control against the white bar, white once it has focus. */
@utility search-input {
  @apply bg-ink-50 dark:bg-ink-950/60;

  &:focus {
    @apply bg-white dark:bg-ink-900;
  }
}

/* A field with a mark in it — the envelope beside an address, the padlock
 * beside a password. The icon is positioned rather than laid out beside the
 * box so the box keeps its own width, and it takes no pointer events so the
 * whole field still focuses when a person clicks the icon. */
@utility field-icon {
  @apply pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2
         text-ink-400 dark:text-ink-500;
}

@utility field-input-invalid {
  @apply border-red-500 focus:border-red-500 focus:ring-red-500/30;
}

@utility field-error {
  @apply mt-1.5 text-sm text-red-600 dark:text-red-400;
}

@utility field-hint {
  @apply mt-1.5 text-sm text-ink-500 dark:text-ink-400;
}

@utility btn {
  @apply inline-flex items-center justify-center gap-2 rounded-lg px-4 py-2.5 text-sm
         font-semibold transition select-none
         focus:outline-none focus:ring-2 focus:ring-offset-2
         focus:ring-offset-white dark:focus:ring-offset-ink-950
         disabled:cursor-not-allowed disabled:opacity-60;
}

@utility btn-primary {
  @apply btn bg-brand-700 text-white hover:bg-brand-800 focus:ring-brand-600;
}

@utility btn-secondary {
  @apply btn border border-ink-200 bg-white text-ink-700
         hover:bg-ink-50 focus:ring-ink-400
         dark:border-ink-700 dark:bg-ink-900 dark:text-ink-200 dark:hover:bg-ink-800;
}

@utility btn-success {
  @apply btn bg-emerald-600 text-white hover:bg-emerald-700 focus:ring-emerald-500;
}

@utility btn-danger {
  @apply btn bg-red-600 text-white shadow-xs hover:bg-red-700 focus:ring-red-500;
}

@utility btn-ghost {
  @apply btn text-ink-600 hover:bg-ink-100 focus:ring-ink-400
         dark:text-ink-300 dark:hover:bg-ink-800;
}

@utility btn-sm {
  @apply px-2.5 py-1.5 text-xs;
}

/* Flat, not lifted. The design spends border on every card and shadow on
 * nothing that sits in the page — a shadow there is reserved for something
 * floating above it, which is how a dropdown reads as a dropdown. */
@utility card {
  @apply rounded-card border border-ink-200 bg-white
         dark:border-ink-800 dark:bg-ink-900;
}

@utility card-header {
  @apply border-b border-ink-200 px-4 py-3.5 dark:border-ink-800;
}

@utility card-body {
  @apply px-4 py-3.5;
}

@utility badge {
  @apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold
         ring-1 ring-inset;
}

@utility badge-neutral {
  @apply badge bg-ink-100 text-ink-700 ring-ink-500/20
         dark:bg-ink-800 dark:text-ink-300 dark:ring-ink-400/20;
}

@utility badge-success {
  @apply badge bg-green-50 text-green-700 ring-green-600/20
         dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/30;
}

@utility badge-warning {
  @apply badge bg-amber-50 text-amber-800 ring-amber-600/20
         dark:bg-amber-500/10 dark:text-amber-400 dark:ring-amber-500/30;
}

@utility badge-danger {
  @apply badge bg-red-50 text-red-700 ring-red-600/20
         dark:bg-red-500/10 dark:text-red-400 dark:ring-red-500/30;
}

@utility table-wrap {
  @apply overflow-x-auto;
}

@utility table {
  @apply w-full text-left text-sm;

  & th {
    @apply whitespace-nowrap px-4 py-3 text-[10px] font-bold uppercase tracking-[0.05em]
           text-ink-400 dark:text-ink-500;
  }

  & td {
    @apply border-t border-ink-200 px-4 py-3 align-middle
           text-ink-700 dark:border-ink-800 dark:text-ink-300;
  }

  & tbody tr:hover {
    @apply bg-ink-50 dark:bg-ink-800/50;
  }
}

/* A role chip. Brand-tinted rather than grey, because on the users table the
 * roles are the column people scan for and grey makes them recede. */
@utility badge-brand {
  @apply badge bg-brand-50 text-brand-700 ring-brand-600/20
         dark:bg-brand-500/10 dark:text-brand-400 dark:ring-brand-500/30;
}

/* One of the small round action buttons at the end of a table row.
 *
 * Icons rather than words because five spelled-out actions push the table wider
 * than the screen. Each still carries a `title` and an `aria-label`, so the
 * meaning is available to a hovering mouse and to a screen reader — an icon
 * with neither is a puzzle, not a control. */
@utility icon-action {
  @apply inline-flex h-8 w-8 items-center justify-center rounded-full transition
         focus:outline-none focus:ring-2 focus:ring-offset-1
         focus:ring-offset-white dark:focus:ring-offset-ink-900;
}

@utility icon-action-neutral {
  @apply icon-action bg-ink-100 text-ink-600 hover:bg-ink-200 focus:ring-ink-400
         dark:bg-ink-800 dark:text-ink-300 dark:hover:bg-ink-700;
}

@utility icon-action-brand {
  @apply icon-action bg-brand-50 text-brand-600 hover:bg-brand-100 focus:ring-brand-500
         dark:bg-brand-500/10 dark:text-brand-400 dark:hover:bg-brand-500/20;
}

@utility icon-action-violet {
  @apply icon-action bg-violet-50 text-violet-600 hover:bg-violet-100 focus:ring-violet-500
         dark:bg-violet-500/10 dark:text-violet-400 dark:hover:bg-violet-500/20;
}

@utility icon-action-emerald {
  @apply icon-action bg-emerald-50 text-emerald-600 hover:bg-emerald-100 focus:ring-emerald-500
         dark:bg-emerald-500/10 dark:text-emerald-400 dark:hover:bg-emerald-500/20;
}

@utility icon-action-amber {
  @apply icon-action bg-amber-50 text-amber-600 hover:bg-amber-100 focus:ring-amber-500
         dark:bg-amber-500/10 dark:text-amber-400 dark:hover:bg-amber-500/20;
}

@utility icon-action-danger {
  @apply icon-action bg-red-50 text-red-600 hover:bg-red-100 focus:ring-red-500
         dark:bg-red-500/10 dark:text-red-400 dark:hover:bg-red-500/20;
}

/* The tinted square behind a statistic's icon. */
@utility stat-tint {
  @apply flex h-9 w-9 items-center justify-center rounded-lg;
}

/* The sidebar, and the two surfaces Settings → Appearance colours.
 *
 * **These four variables were written and never read.** `theme_controller`
 * has always generated `--sidebar-bg`, `--sidebar-text`, `--sidebar-active-bg`
 * and `--sidebar-active-text` into `/css/theme.css`, and no rule anywhere
 * consumed one — so the eight colours on that tab wrote a file and changed
 * nothing on the screen. Every fallback below is the value the settings
 * catalogue declares, so the page looks right before the stylesheet has
 * loaded and identical after it. */
@utility app-sidebar {
  background-color: var(--sidebar-bg, oklch(0.296 0.072 251));
  color: var(--sidebar-text, oklch(0.760 0.045 250));
}

@utility nav-link {
  @apply flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition;

  color: var(--sidebar-text, oklch(0.760 0.045 250));

  /* In the rail there is no label to sit beside, so the icon takes the middle
     rather than staying where the text left it. */
  :root[data-rail] & {
    @apply justify-center px-0;
  }

  &:hover {
    background-color: color-mix(in oklab, var(--sidebar-active-bg, oklch(0.360 0.080 251)) 55%, transparent);
    color: var(--sidebar-active-text, #fff);
  }
}

@utility nav-link-active {
  background-color: var(--sidebar-active-bg, oklch(0.360 0.080 251));
  color: var(--sidebar-active-text, #fff);
  font-weight: 600;
}

/* The heading above a group of sidebar entries. */
/* The collapsed rail.
 *
 * `data-rail` on <html> rather than a class on the sidebar, so one attribute
 * governs the rail's width and every label inside it — and the state survives
 * a page load, because the script sets it before the first paint from what it
 * remembers. */
@utility nav-label {
  :root[data-rail] & {
    @apply hidden;
  }
}

@utility nav-heading {
  @apply mt-5 px-3 pb-1 text-[10px] font-bold uppercase tracking-[0.08em];

  color: color-mix(in oklab, var(--sidebar-text, oklch(0.760 0.045 250)) 70%, transparent);

  /* A group heading clipped to "ADMIN" is worse than no heading. In the rail
     it becomes the rule the design draws between the two groups instead. */
  :root[data-rail] & {
    @apply mx-3 mt-3 h-px overflow-hidden p-0 text-transparent;

    background-color: color-mix(in oklab, var(--sidebar-text, oklch(0.760 0.045 250)) 22%, transparent);
  }
}

/* A hairline that reads on the sidebar's own ground rather than the page's. */
/* Only above `lg`: below it the sidebar is a drawer, and a drawer that opens
 * to a strip of icons is not a menu. */
@utility rail-width {
  @media (width >= 64rem) {
    :root[data-rail] & {
      width: calc(var(--spacing) * 16);
    }
  }
}

@utility sidebar-rule {
  border-color: color-mix(in oklab, var(--sidebar-text, oklch(0.760 0.045 250)) 22%, transparent);
}

/* The panel beside a sign-in form.
 *
 * The other half of the same omission: `--login-from` and `--login-to` were
 * generated for a gradient no rule drew. This is that gradient. */
@utility auth-hero {
  background-image: linear-gradient(
    160deg,
    var(--login-from, oklch(0.296 0.072 251)),
    var(--login-to, oklch(0.424 0.127 252))
  );
}

/* A person's initial in a small circle, for a table that lists who did what. */
@utility avatar-sm {
  @apply inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full
         bg-brand-600 text-[10px] font-semibold text-white;
}

/* One number in a pager. */
@utility page-link {
  @apply inline-flex h-8 min-w-8 items-center justify-center rounded-lg px-2 text-sm
         font-medium text-ink-600 transition
         hover:bg-ink-100 hover:text-ink-900
         dark:text-ink-400 dark:hover:bg-ink-800 dark:hover:text-ink-100;
}

@utility page-link-current {
  @apply bg-brand-600 text-white hover:bg-brand-600 hover:text-white
         dark:bg-brand-600 dark:text-white dark:hover:bg-brand-600;
}

/* The destructive twin of `btn-primary`, for Export PDF and the like. */
@utility btn-danger {
  @apply inline-flex items-center justify-center gap-2 rounded-lg bg-red-600 px-4 py-2
         text-sm font-medium text-white shadow-xs transition
         hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2
         disabled:cursor-not-allowed disabled:opacity-60
         dark:focus:ring-offset-ink-900;
}

/* A menu location, as a pill in the filter row. */
@utility tab-pill {
  @apply inline-flex items-center rounded-lg px-3 py-1.5 text-sm font-medium
         text-ink-600 transition hover:bg-ink-100 hover:text-ink-900
         dark:text-ink-400 dark:hover:bg-ink-800 dark:hover:text-ink-100;
}

@utility tab-pill-current {
  @apply bg-brand-600 text-white hover:bg-brand-600 hover:text-white
         dark:bg-brand-600 dark:text-white dark:hover:bg-brand-600;
}

/* The grip a menu row is dragged by. A handle rather than the whole row: the
 * row holds links and buttons, and making all of it draggable takes the text
 * selection and the click targets with it. */
@utility menu-handle {
  @apply inline-flex h-7 w-7 shrink-0 cursor-grab items-center justify-center rounded-md
         text-ink-400 transition hover:bg-ink-100 hover:text-ink-600
         focus:outline-none focus:ring-2 focus:ring-brand-500
         dark:hover:bg-ink-800 dark:hover:text-ink-300;
}

/* One icon in the picker. The radio inside is visually hidden, so the label
 * carries the selected state through `:has`. */
@utility icon-choice {
  @apply inline-flex cursor-pointer flex-col items-center justify-center gap-1 rounded-lg
         border border-ink-200 py-2.5 text-ink-500 transition
         hover:border-ink-300 hover:text-ink-700
         has-[:checked]:border-brand-500 has-[:checked]:bg-brand-50 has-[:checked]:text-brand-600
         has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-brand-500
         dark:border-ink-800 dark:text-ink-400 dark:hover:border-ink-700
         dark:has-[:checked]:bg-brand-500/10 dark:has-[:checked]:text-brand-400;
}