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