palace-ui 0.12.2 → 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 +182 -54
  2. package/catalog.html +886 -664
  3. package/package.json +8 -2
  4. package/palace.css +152 -45
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "palace-ui",
3
- "version": "0.12.2",
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);
@@ -636,6 +653,75 @@ textarea:focus {
636
653
  transform: var(--p-lift-sm);
637
654
  }
638
655
 
656
+ /* A field with a button inside, at its right end (show/hide a password, paste,
657
+ copy). The input reserves the button's width plus its --p-space-sm inset; the button is centred in the
658
+ input's box and lifts with it, so the two stay aligned on focus. Set
659
+ --input-action-width to the button's width when it is wider. */
660
+ .input-action {
661
+ position: relative;
662
+ --input-action-width: 4.75rem;
663
+ }
664
+ .input-action > input[type] { padding-inline-end: calc(var(--input-action-width) + var(--p-space-sm)); }
665
+ .input-action > .btn,
666
+ .input-action > button {
667
+ position: absolute;
668
+ inset-block: 0;
669
+ inset-inline-end: var(--p-space-sm);
670
+ min-height: 0;
671
+ height: var(--p-control-height-sm);
672
+ white-space: nowrap;
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);
676
+ }
677
+ .input-action > .btn-sm { padding-block: calc((var(--p-control-height-sm) - 1.3em - 2 * var(--p-border-thin)) / 2); }
678
+ .input-action > input:focus ~ .btn,
679
+ .input-action > input:focus ~ button {
680
+ transform: var(--p-lift-sm);
681
+ }
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
+
639
725
  /* Checkboxes, radio buttons and disclosure summaries: the focus ring of links
640
726
  and buttons, without the lift. */
641
727
  input[type="checkbox"]:focus-visible,
@@ -656,6 +742,7 @@ summary:focus-visible {
656
742
  font-size: 13px;
657
743
  font-weight: 600;
658
744
  border: var(--p-border-width) solid var(--p-text);
745
+ box-shadow: var(--p-shadow-lg) var(--p-text);
659
746
  }
660
747
 
661
748
  .status-success {
@@ -826,9 +913,11 @@ summary:focus-visible {
826
913
  border: var(--p-border-width) solid var(--p-text);
827
914
  box-shadow: var(--p-shadow-md) var(--p-accent-2);
828
915
  }
829
- .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; }
830
920
  .navbar-menu-label {
831
- margin: 0;
832
921
  padding: var(--p-space-sm) var(--p-space-md) var(--p-space-xs);
833
922
  font-size: 11px;
834
923
  font-weight: 600;
@@ -840,6 +929,8 @@ summary:focus-visible {
840
929
  .navbar-menu-panel button {
841
930
  display: block;
842
931
  width: 100%;
932
+ height: auto;
933
+ min-height: 0;
843
934
  box-sizing: border-box;
844
935
  padding: var(--p-space-sm) var(--p-space-md);
845
936
  border: 0;
@@ -964,7 +1055,6 @@ pre {
964
1055
  .code-reveal code {
965
1056
  word-break: break-all;
966
1057
  display: block;
967
- margin-bottom: 12px;
968
1058
  background: var(--p-code-bg);
969
1059
  padding: 12px;
970
1060
  border: var(--p-border-thin) solid var(--p-border-dark);
@@ -972,6 +1062,8 @@ pre {
972
1062
  user-select: all;
973
1063
  }
974
1064
 
1065
+ :where(.code-reveal) > * + * { margin-top: var(--p-space-sm); }
1066
+
975
1067
 
976
1068
  /* ══════════════════════════════════════════
977
1069
  TAGS & BADGES
@@ -1189,14 +1281,14 @@ pre {
1189
1281
  text-transform: uppercase;
1190
1282
  letter-spacing: 1.5px;
1191
1283
  color: var(--p-text-faint);
1192
- margin-top: 2px;
1193
1284
  }
1285
+ /* Optical: 2px off the stat above. In :where() so space-* wins. */
1286
+ :where(.stat-label) { margin-top: 2px; }
1194
1287
 
1195
1288
  .stat-row {
1196
1289
  display: flex;
1197
1290
  flex-wrap: wrap;
1198
1291
  gap: var(--p-space-lg);
1199
- margin-bottom: 12px;
1200
1292
  }
1201
1293
 
1202
1294
 
@@ -1240,21 +1332,30 @@ dialog {
1240
1332
  which is what centres a modal dialog. Restored here. */
1241
1333
  margin: auto;
1242
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; }
1243
1338
 
1244
1339
  dialog::backdrop { background: var(--p-overlay); }
1245
- dialog h2 { margin-bottom: var(--p-space-xs); }
1246
- dialog .subtitle { margin-bottom: var(--p-space-lg); }
1247
- 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); }
1248
1348
 
1249
1349
  dialog menu {
1250
1350
  display: flex;
1251
1351
  gap: var(--p-space-sm);
1252
1352
  padding: 0;
1253
- margin: var(--p-space-lg) 0 0;
1254
1353
  list-style: none;
1255
1354
  }
1256
1355
 
1257
- 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%; }
1258
1359
 
1259
1360
  /* ══════════════════════════════════════════
1260
1361
  KBD
@@ -1266,6 +1367,12 @@ kbd {
1266
1367
  border: var(--p-border-thin) solid var(--p-text);
1267
1368
  box-shadow: 1px 1px 0 var(--p-accent-1);
1268
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);
1269
1372
  font: 600 11px/1.5 var(--p-font-mono);
1270
1373
  text-transform: uppercase;
1271
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; }