palace-ui 0.13.0 → 0.15.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.
Files changed (4) hide show
  1. package/SPEC.md +197 -63
  2. package/catalog.html +892 -674
  3. package/package.json +8 -2
  4. package/palace.css +138 -53
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palace-ui",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Retro-technical design system. Hard edges, vibrant gradients, chunky shadows.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,5 +21,11 @@
21
21
  "design-system",
22
22
  "design-tokens",
23
23
  "ui"
24
- ]
24
+ ],
25
+ "scripts": {
26
+ "test": "node --test 'test/*.test.js'"
27
+ },
28
+ "devDependencies": {
29
+ "playwright-core": "^1.64.1"
30
+ }
25
31
  }
package/palace.css CHANGED
@@ -1,4 +1,4 @@
1
- /*! palace-ui v0.12.0 | MIT License | fonts: SIL OFL 1.1, see fonts/ */
1
+ /*! palace-ui v0.14.0 | MIT License | fonts: SIL OFL 1.1, see fonts/ */
2
2
 
3
3
  /* ══════════════════════════════════════════
4
4
  FONTS (self-hosted; no third-party requests)
@@ -69,6 +69,11 @@
69
69
  --p-space-xl: 32px;
70
70
  --p-space-2xl: 48px;
71
71
 
72
+ /* ── Controls ── */
73
+ --p-control-height: 44px;
74
+ --p-control-height-sm: 32px;
75
+ --p-control-height-lg: 52px;
76
+
72
77
  /* ── Layout ── */
73
78
  --p-container-width: 640px;
74
79
  --p-container-narrow: 420px;
@@ -80,6 +85,7 @@
80
85
  --p-radius: 0;
81
86
 
82
87
  /* ── Shadows (offsets only — combine with color) ── */
88
+ --p-shadow-xs: 1px 1px 0;
83
89
  --p-shadow-sm: 2px 2px 0;
84
90
  --p-shadow-md: 3px 3px 0;
85
91
  --p-shadow-lg: 4px 4px 0;
@@ -239,27 +245,31 @@ h3, .h3 {
239
245
  padding: 32px 16px;
240
246
  }
241
247
 
242
- /* Page header — standard spacing after navbar for non-hero pages */
248
+ /* Page header — top padding for non-hero pages; no outer margin, the
249
+ space after it comes from the parent's space-* or gap-* */
243
250
  .page-header {
244
251
  padding-top: var(--p-space-2xl);
245
252
  }
246
253
 
247
- .page-header h1,
248
- .page-header .h1 {
249
- margin-bottom: var(--p-space-sm);
250
- }
254
+ :where(.page-header) > * + * { margin-top: var(--p-space-sm); }
251
255
 
252
- .page-header .subtitle {
253
- margin-bottom: 32px;
254
- }
256
+ /* ── Space and gap (the spacing scale between children) ──
257
+ space-*: vertical rhythm between children, with the owl selector.
258
+ gap-*: the gap of a flex or grid container.
259
+ stack-*: deprecated alias of space-*, kept so existing markup works. */
260
+ .space-xs > * + *, .stack-xs > * + * { margin-top: var(--p-space-xs); }
261
+ .space-sm > * + *, .stack-sm > * + * { margin-top: var(--p-space-sm); }
262
+ .space-md > * + *, .stack-md > * + * { margin-top: var(--p-space-md); }
263
+ .space-lg > * + *, .stack-lg > * + * { margin-top: var(--p-space-lg); }
264
+ .space-xl > * + *, .stack-xl > * + * { margin-top: var(--p-space-xl); }
265
+ .space-2xl > * + *, .stack-2xl > * + * { margin-top: var(--p-space-2xl); }
255
266
 
256
- /* ── Stack spacing (vertical rhythm between children) ── */
257
- .stack-xs > * + * { margin-top: var(--p-space-xs); }
258
- .stack-sm > * + * { margin-top: var(--p-space-sm); }
259
- .stack-md > * + * { margin-top: var(--p-space-md); }
260
- .stack-lg > * + * { margin-top: var(--p-space-lg); }
261
- .stack-xl > * + * { margin-top: var(--p-space-xl); }
262
- .stack-2xl > * + * { margin-top: var(--p-space-2xl); }
267
+ .gap-xs { gap: var(--p-space-xs); }
268
+ .gap-sm { gap: var(--p-space-sm); }
269
+ .gap-md { gap: var(--p-space-md); }
270
+ .gap-lg { gap: var(--p-space-lg); }
271
+ .gap-xl { gap: var(--p-space-xl); }
272
+ .gap-2xl { gap: var(--p-space-2xl); }
263
273
 
264
274
  /* ── Lists (restore styling inside containers) ── */
265
275
  .container ul,
@@ -281,11 +291,13 @@ h3, .h3 {
281
291
 
282
292
  .container li,
283
293
  .container-narrow li {
284
- margin-bottom: 4px;
285
294
  font-size: 14px;
286
295
  line-height: 1.5;
287
296
  }
288
297
 
298
+ /* Items of a list, never items laid out by a component (a dialog's <menu>). */
299
+ :where(.container, .container-narrow) :where(ul, ol) > :where(li + li) { margin-top: var(--p-space-xs); }
300
+
289
301
 
290
302
  /* ══════════════════════════════════════════
291
303
  LINKS
@@ -314,8 +326,15 @@ a:focus-visible {
314
326
  BUTTONS
315
327
  ══════════════════════════════════════════ */
316
328
 
329
+ /* Buttons are inline blocks, so the spaces between inline children (a label
330
+ and its <kbd>, <strong>) stay. One line of label is centred by padding
331
+ derived from the control height: (height - one line - both borders) / 2.
332
+ A label that wraps grows the button past min-height. */
317
333
  button, .btn {
318
- padding: 10px 16px;
334
+ min-height: var(--p-control-height);
335
+ padding: calc((var(--p-control-height) - 1.3em - 2 * var(--p-border-width)) / 2) 16px;
336
+ /* One line height for <button> and <a class="btn">, so a long label wraps the same in both. */
337
+ line-height: 1.3;
319
338
  font-family: var(--p-font);
320
339
  font-size: 13px;
321
340
  font-weight: 700;
@@ -331,25 +350,22 @@ button, .btn {
331
350
  transition-property: background-color, border-color, color, box-shadow, transform, outline-color;
332
351
  text-decoration: none;
333
352
  display: inline-block;
334
- /* One line height for <button> and <a class="btn">, so both are the same
335
- height: a button does not inherit the body's line height, a link does. */
336
- line-height: calc(17 / 13);
337
353
  text-align: center;
338
354
  }
339
355
 
340
356
  /* Size modifiers */
341
357
  .btn-sm {
342
- padding: 6px 14px;
358
+ min-height: var(--p-control-height-sm);
359
+ padding: calc((var(--p-control-height-sm) - 1.3em - 2 * var(--p-border-thin)) / 2) 14px;
343
360
  font-size: 11px;
344
- line-height: calc(14 / 11);
345
361
  letter-spacing: 1px;
346
362
  border-width: var(--p-border-thin);
347
363
  }
348
364
 
349
365
  .btn-lg {
350
- padding: 14px 28px;
366
+ min-height: var(--p-control-height-lg);
367
+ padding: calc((var(--p-control-height-lg) - 1.3em - 2 * var(--p-border-width)) / 2) 28px;
351
368
  font-size: 15px;
352
- line-height: calc(19 / 15);
353
369
  letter-spacing: 1px;
354
370
  }
355
371
 
@@ -409,11 +425,13 @@ button, .btn {
409
425
 
410
426
  /* CTA buttons (larger, for hero sections) */
411
427
  .cta {
428
+ /* An inline block like .btn, centred by padding from the large control height. */
412
429
  display: inline-block;
413
- padding: 14px 28px;
430
+ min-height: var(--p-control-height-lg);
431
+ padding: calc((var(--p-control-height-lg) - 1.3em - 2 * var(--p-border-width)) / 2) 28px;
432
+ line-height: 1.3;
414
433
  font-family: var(--p-font);
415
434
  font-size: 15px;
416
- line-height: calc(19 / 15);
417
435
  text-align: center;
418
436
  font-weight: 700;
419
437
  text-transform: uppercase;
@@ -474,7 +492,6 @@ button:disabled, .btn:disabled, .cta:disabled {
474
492
  background: var(--p-surface);
475
493
  border: var(--p-border-width) solid var(--p-text);
476
494
  padding: var(--p-space-lg);
477
- margin-bottom: var(--p-space-md);
478
495
  transition: var(--p-transition);
479
496
  }
480
497
 
@@ -510,17 +527,17 @@ button:disabled, .btn:disabled, .cta:disabled {
510
527
  line-height: 1.6;
511
528
  }
512
529
 
513
- /* Static card — no hover lift/shadow (for form containers) */
530
+ /* Static card — no hover lift (for form containers); its large shadow stays, at rest and on hover */
514
531
  .card-static {
515
532
  background: var(--p-surface);
516
533
  border: var(--p-border-width) solid var(--p-text);
534
+ box-shadow: var(--p-shadow-lg) var(--p-text);
517
535
  padding: var(--p-space-lg);
518
- margin-bottom: var(--p-space-md);
519
536
  }
520
537
 
521
538
  .card-static:hover {
522
539
  transform: none;
523
- box-shadow: none;
540
+ box-shadow: var(--p-shadow-lg) var(--p-text);
524
541
  }
525
542
 
526
543
 
@@ -535,7 +552,6 @@ label {
535
552
  text-transform: uppercase;
536
553
  letter-spacing: 1px;
537
554
  color: var(--p-text-muted);
538
- margin-bottom: 6px;
539
555
  }
540
556
 
541
557
  input[type="text"],
@@ -545,7 +561,8 @@ input[type="search"],
545
561
  input[type="url"],
546
562
  input[type="tel"] {
547
563
  width: 100%;
548
- padding: 12px;
564
+ height: var(--p-control-height);
565
+ padding: 0 12px;
549
566
  border: var(--p-border-width) solid var(--p-text);
550
567
  border-radius: var(--p-radius);
551
568
  font-family: var(--p-font);
@@ -569,7 +586,8 @@ input[type="tel"]:focus {
569
586
  select {
570
587
  width: 100%;
571
588
  appearance: none;
572
- padding: 12px 2.5rem 12px 12px;
589
+ height: var(--p-control-height);
590
+ padding: 0 2.5rem 0 12px;
573
591
  border: var(--p-border-width) solid var(--p-text);
574
592
  border-radius: var(--p-radius);
575
593
  background-color: var(--p-surface);
@@ -584,7 +602,7 @@ select {
584
602
  outline: none;
585
603
  transition: var(--p-transition);
586
604
  }
587
- select[multiple], select[size]:not([size="1"]) { padding-right: 12px; background-image: none; }
605
+ select[multiple], select[size]:not([size="1"]) { height: auto; padding: 12px; background-image: none; }
588
606
 
589
607
  select:focus {
590
608
  border-color: var(--p-accent-2);
@@ -604,7 +622,8 @@ input[type="number"] {
604
622
  text-align: center;
605
623
  border: var(--p-border-width) solid var(--p-text);
606
624
  border-radius: var(--p-radius);
607
- padding: 4px;
625
+ height: var(--p-control-height);
626
+ padding: 0 4px;
608
627
  outline: none;
609
628
  background: var(--p-surface);
610
629
  transition: var(--p-transition);
@@ -624,7 +643,7 @@ textarea {
624
643
  border-radius: var(--p-radius);
625
644
  padding: var(--p-space-sm);
626
645
  resize: vertical;
627
- min-height: 50px;
646
+ min-height: var(--p-control-height);
628
647
  background: var(--p-surface);
629
648
  outline: none;
630
649
  transition: var(--p-transition);
@@ -637,28 +656,74 @@ textarea:focus {
637
656
  }
638
657
 
639
658
  /* A field with a button inside, at its right end (show/hide a password, paste,
640
- copy). The input reserves the button's width plus its 8px inset; the button is centred in the
659
+ copy). The input reserves the button's width plus its --p-space-sm inset; the button is centred in the
641
660
  input's box and lifts with it, so the two stay aligned on focus. Set
642
661
  --input-action-width to the button's width when it is wider. */
643
662
  .input-action {
644
663
  position: relative;
645
664
  --input-action-width: 4.75rem;
646
665
  }
647
- .input-action > input[type] { padding-inline-end: calc(var(--input-action-width) + 8px); }
666
+ .input-action > input[type] { padding-inline-end: calc(var(--input-action-width) + var(--p-space-sm)); }
648
667
  .input-action > .btn,
649
668
  .input-action > button {
650
669
  position: absolute;
651
670
  inset-block: 0;
652
- inset-inline-end: 8px;
653
- height: fit-content;
671
+ inset-inline-end: var(--p-space-sm);
672
+ min-height: 0;
673
+ height: var(--p-control-height-sm);
654
674
  white-space: nowrap;
655
675
  margin-block: auto;
676
+ /* One line centred in the small height, as .btn centres it in its own. */
677
+ padding-block: calc((var(--p-control-height-sm) - 1.3em - 2 * var(--p-border-width)) / 2);
656
678
  }
679
+ .input-action > .btn-sm { padding-block: calc((var(--p-control-height-sm) - 1.3em - 2 * var(--p-border-thin)) / 2); }
657
680
  .input-action > input:focus ~ .btn,
658
681
  .input-action > input:focus ~ button {
659
682
  transform: var(--p-lift-sm);
660
683
  }
661
684
 
685
+ /* A label, its control and an optional hint or error, spaced by --p-space-sm. */
686
+ .field {
687
+ display: grid;
688
+ gap: var(--p-space-sm);
689
+ }
690
+ .field-hint,
691
+ .field-error {
692
+ /* palace has no font-size tokens; 12px matches the file's other small text */
693
+ font-size: 12px;
694
+ }
695
+ .field-hint { color: var(--p-text-muted); }
696
+ .field-error {
697
+ color: var(--p-error-text);
698
+ font-weight: 600;
699
+ }
700
+ :is(input, select, textarea)[aria-invalid="true"],
701
+ :is(input, select, textarea)[aria-invalid="true"]:focus {
702
+ border-color: var(--p-error-text);
703
+ }
704
+ :is(input:not([type="checkbox"], [type="radio"]), select, textarea)[aria-invalid="true"]:focus {
705
+ box-shadow: var(--p-shadow-md) var(--p-error-text);
706
+ }
707
+
708
+ /* Fields and buttons in one row, bottoms aligned; the row wraps when narrow.
709
+ Fields, inputs, selects and textareas grow from --control-row-min; buttons keep their
710
+ natural width but may shrink and wrap their label. */
711
+ .control-row {
712
+ --control-row-min: 12rem;
713
+ display: flex;
714
+ flex-wrap: wrap;
715
+ align-items: end;
716
+ gap: var(--p-space-sm);
717
+ }
718
+ .control-row > :is(.field, .input-action, input, select, textarea) {
719
+ flex: 1 1 var(--control-row-min);
720
+ min-width: 0;
721
+ }
722
+ .control-row > :is(button, .btn) {
723
+ flex: 0 1 auto;
724
+ max-width: 100%;
725
+ }
726
+
662
727
  /* Checkboxes, radio buttons and disclosure summaries: the focus ring of links
663
728
  and buttons, without the lift. */
664
729
  input[type="checkbox"]:focus-visible,
@@ -848,11 +913,13 @@ summary:focus-visible {
848
913
  background: var(--p-surface);
849
914
  color: var(--p-text);
850
915
  border: var(--p-border-width) solid var(--p-text);
851
- box-shadow: var(--p-shadow-md) var(--p-accent-2);
916
+ box-shadow: var(--p-shadow-lg) var(--p-accent-2);
852
917
  }
853
- .navbar-menu-panel ul { list-style: none; margin: 0; padding: 0; display: grid; }
918
+ /* The margin resets sit in :where() so a space-* utility on the panel beats them. */
919
+ :where(.navbar-menu-panel ul) { margin: 0; }
920
+ .navbar-menu-panel ul { list-style: none; padding: 0; display: grid; }
921
+ :where(.navbar-menu-label) { margin: 0; }
854
922
  .navbar-menu-label {
855
- margin: 0;
856
923
  padding: var(--p-space-sm) var(--p-space-md) var(--p-space-xs);
857
924
  font-size: 11px;
858
925
  font-weight: 600;
@@ -864,6 +931,8 @@ summary:focus-visible {
864
931
  .navbar-menu-panel button {
865
932
  display: block;
866
933
  width: 100%;
934
+ height: auto;
935
+ min-height: 0;
867
936
  box-sizing: border-box;
868
937
  padding: var(--p-space-sm) var(--p-space-md);
869
938
  border: 0;
@@ -988,7 +1057,6 @@ pre {
988
1057
  .code-reveal code {
989
1058
  word-break: break-all;
990
1059
  display: block;
991
- margin-bottom: 12px;
992
1060
  background: var(--p-code-bg);
993
1061
  padding: 12px;
994
1062
  border: var(--p-border-thin) solid var(--p-border-dark);
@@ -996,6 +1064,8 @@ pre {
996
1064
  user-select: all;
997
1065
  }
998
1066
 
1067
+ :where(.code-reveal) > * + * { margin-top: var(--p-space-sm); }
1068
+
999
1069
 
1000
1070
  /* ══════════════════════════════════════════
1001
1071
  TAGS & BADGES
@@ -1213,14 +1283,14 @@ pre {
1213
1283
  text-transform: uppercase;
1214
1284
  letter-spacing: 1.5px;
1215
1285
  color: var(--p-text-faint);
1216
- margin-top: 2px;
1217
1286
  }
1287
+ /* Optical: 2px off the stat above. In :where() so space-* wins. */
1288
+ :where(.stat-label) { margin-top: 2px; }
1218
1289
 
1219
1290
  .stat-row {
1220
1291
  display: flex;
1221
1292
  flex-wrap: wrap;
1222
1293
  gap: var(--p-space-lg);
1223
- margin-bottom: 12px;
1224
1294
  }
1225
1295
 
1226
1296
 
@@ -1264,21 +1334,30 @@ dialog {
1264
1334
  which is what centres a modal dialog. Restored here. */
1265
1335
  margin: auto;
1266
1336
  }
1337
+ /* An open modal stays centred even in a space-* container, whose
1338
+ `> * + *` margin is more specific than `dialog`. */
1339
+ dialog:modal { margin: auto; }
1267
1340
 
1268
1341
  dialog::backdrop { background: var(--p-overlay); }
1269
- dialog h2 { margin-bottom: var(--p-space-xs); }
1270
- dialog .subtitle { margin-bottom: var(--p-space-lg); }
1271
- dialog p { margin-bottom: var(--p-space-sm); }
1342
+ /* The dialog spaces its own children, or those of a direct <form>.
1343
+ Specificity decides first: `.subtitle + *` has a class and beats the
1344
+ element rules. Among those (p, h2, menu), all equally specific, the later
1345
+ one in this file wins, so a <menu> after a <p> still gets lg. */
1346
+ :where(dialog, dialog > form) > p + * { margin-top: var(--p-space-sm); }
1347
+ :where(dialog, dialog > form) > h2 + * { margin-top: var(--p-space-xs); }
1348
+ :where(dialog, dialog > form) > .subtitle + *,
1349
+ :where(dialog, dialog > form) > menu { margin-top: var(--p-space-lg); }
1272
1350
 
1273
1351
  dialog menu {
1274
1352
  display: flex;
1275
1353
  gap: var(--p-space-sm);
1276
1354
  padding: 0;
1277
- margin: var(--p-space-lg) 0 0;
1278
1355
  list-style: none;
1279
1356
  }
1280
1357
 
1281
- dialog menu .btn { flex: 1; }
1358
+ /* The buttons share the row's width. */
1359
+ dialog menu > li { flex: 1; }
1360
+ dialog menu > li > :is(button, .btn) { width: 100%; }
1282
1361
 
1283
1362
  /* ══════════════════════════════════════════
1284
1363
  KBD
@@ -1288,8 +1367,14 @@ kbd {
1288
1367
  display: inline-block;
1289
1368
  padding: 1px 5px;
1290
1369
  border: var(--p-border-thin) solid var(--p-text);
1291
- box-shadow: 1px 1px 0 var(--p-accent-1);
1370
+ box-shadow: var(--p-shadow-xs) var(--p-accent-1);
1292
1371
  background: var(--p-surface-inset);
1372
+ /* Its own text colour: on its light background, a key must not take a button's white. */
1373
+ color: var(--p-text);
1293
1374
  font: 600 11px/1.5 var(--p-font-mono);
1294
1375
  text-transform: uppercase;
1295
1376
  }
1377
+ /* A key in a button's label stays inside the button's one line, so the button
1378
+ keeps the control height: sized from the button's font (11px in a 13px
1379
+ button), with no padding or line height of its own above it. */
1380
+ :where(button, .btn, .cta) kbd { padding-block: 0; font-size: 0.85em; line-height: 1; vertical-align: middle; }