@phcdevworks/spectre-ui 2.2.0 → 2.3.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.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,42 @@ reflects package releases published to npm.
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.3.0] - 2026-06-19
10
+
11
+ Release Title: App Shell Recipe Expansion
12
+
13
+ Contract change type: additive
14
+
15
+ ### Added
16
+
17
+ - **Stack `basis` option**: Added a `basis` option (`sidebar`) to
18
+ `getStackClasses`, mapping a flex child to a fixed width via the new
19
+ `--sp-layout-sidebar-width` token (`@phcdevworks/spectre-tokens@3.1.0`),
20
+ distinct from the default `flex: 1` auto-sizing behavior.
21
+ - **Container `maxWidth` option**: Added a `maxWidth` option (`prose`) to
22
+ `getContainerClasses`, mapping to the new
23
+ `--sp-layout-container-max-width-prose` token, distinct from the existing
24
+ page-level `--sp-layout-container-max-width`.
25
+ - **Sidebar recipe**: Added `getSidebarClasses`, `getSidebarLinkClasses`, and
26
+ `getSidebarBackdropClasses`, wrapping new `.sp-sidebar` / `.sp-sidebar__link`
27
+ / `.sp-sidebar-backdrop` component classes in `src/styles/components.css`.
28
+ Reuses the existing `component.nav` token roles (bg/text/link/border) as
29
+ the vertical counterpart to `SpNav`'s top-bar pattern; sidebar width comes
30
+ from the same `--sp-layout-sidebar-width` token used by Stack's `basis`
31
+ option. Below `breakpoints.md`, the sidebar is an off-canvas drawer
32
+ (`transform: translateX(-100%)`) with a backdrop, toggled via a
33
+ `data-sidebar-open="true"` attribute contract — this is the first recipe
34
+ family with an interactive-state CSS contract. This package owns the CSS
35
+ reaction only; toggle behavior, click handling, and state management
36
+ belong to the consuming adapter.
37
+ - **Footer recipe**: Added `getFooterClasses`, wrapping a new `.sp-footer`
38
+ component class in `src/styles/components.css`, modeled on `SpNav`'s
39
+ `bordered`/`fullWidth` option shape (no `sticky`, per the deferred-unless-
40
+ needed decision in `TODO.md`).
41
+
42
+ This is Phase 4d in `TODO.md` — real downstream need surfaced in
43
+ `docs-phcdevworks-com`'s app shell (top bar + sidebar + main content).
44
+
9
45
  ## [2.2.0] - 2026-06-18
10
46
 
11
47
  Release Title: Grid Recipe Expansion
package/README.md CHANGED
@@ -274,10 +274,12 @@ All options are optional and fall back to sensible defaults.
274
274
  | Tooltip | `getTooltipClasses` | placement: `top` `bottom` `left` `right` | — | `visible` |
275
275
  | Dropdown | `getDropdownClasses` | menu placement: `bottom-start` `bottom-end` `top-start` `top-end` | — | `fullWidth`, item: `active` `disabled` |
276
276
  | Modal | `getModalClasses` | — | — | `open` `fullWidth` |
277
- | Container | `getContainerClasses` | | — | — |
278
- | Stack | `getStackClasses` | direction: `vertical` `horizontal` | — | — |
277
+ | Container | `getContainerClasses` | maxWidth: `prose` | — | — |
278
+ | Stack | `getStackClasses` | direction: `vertical` `horizontal`, basis: `sidebar` | — | — |
279
279
  | Section | `getSectionClasses` | — | — | — |
280
280
  | Grid | `getGridClasses` | columns: `1` `2` `3` `4` `6` `12` | gap: `sm` `md` `lg` | — |
281
+ | Sidebar | `getSidebarClasses` | — | — | `bordered` |
282
+ | Footer | `getFooterClasses` | — | — | `bordered` `fullWidth` |
281
283
 
282
284
  Each recipe family also exports sub-element helpers for its structural parts
283
285
  (labels, wrappers, sub-containers, text elements). See the full list below.
@@ -304,6 +306,7 @@ Root recipe functions:
304
306
  - `getCardClasses`
305
307
  - `getContainerClasses`
306
308
  - `getDropdownClasses`
309
+ - `getFooterClasses`
307
310
  - `getGridClasses`
308
311
  - `getIconBoxClasses`
309
312
  - `getInputClasses`
@@ -312,6 +315,7 @@ Root recipe functions:
312
315
  - `getPricingCardClasses`
313
316
  - `getRatingClasses`
314
317
  - `getSectionClasses`
318
+ - `getSidebarClasses`
315
319
  - `getSpinnerClasses`
316
320
  - `getStackClasses`
317
321
  - `getTagClasses`
@@ -337,6 +341,8 @@ Root recipe helper functions:
337
341
  - `getRatingStarClasses`
338
342
  - `getRatingStarsClasses`
339
343
  - `getRatingTextClasses`
344
+ - `getSidebarBackdropClasses`
345
+ - `getSidebarLinkClasses`
340
346
  - `getTestimonialAuthorClasses`
341
347
  - `getTestimonialAuthorInfoClasses`
342
348
  - `getTestimonialAuthorNameClasses`
@@ -378,6 +384,28 @@ README documentation omits manifest-declared exports, when export snapshots
378
384
  drift, when Tailwind artifacts drift, or when CSS contract coverage no longer
379
385
  matches the declared surface.
380
386
 
387
+ ### Sidebar interactive-state contract
388
+
389
+ `getSidebarClasses` is the first recipe family with an interactive-state CSS
390
+ contract. Below `breakpoints.md` (768px), `.sp-sidebar` renders off-canvas
391
+ (`transform: translateX(-100%)`). This package owns only the CSS reaction to
392
+ that state — it does not own toggle behavior, click handlers, or open/closed
393
+ state management.
394
+
395
+ Consumers (typically a framework adapter) toggle the sidebar by setting a
396
+ `data-sidebar-open="true"` attribute on an ancestor element wrapping
397
+ `.sp-sidebar` and `.sp-sidebar-backdrop` (from `getSidebarBackdropClasses`):
398
+
399
+ - `[data-sidebar-open="true"] .sp-sidebar` slides the sidebar into view
400
+ (`transform: translateX(0)`).
401
+ - `[data-sidebar-open="true"] .sp-sidebar-backdrop` shows the backdrop
402
+ overlay (`display: block`).
403
+ - Above `breakpoints.md`, the sidebar docks inline and the backdrop is
404
+ always hidden, regardless of the `data-sidebar-open` value.
405
+
406
+ Adapters own the hamburger/toggle control, click handling, and SSR-safe
407
+ initial closed state.
408
+
381
409
  ## Downstream boundaries
382
410
 
383
411
  Downstream packages should never redefine locally:
package/dist/base.css CHANGED
@@ -5,6 +5,10 @@
5
5
  --sp-surface-overlay: rgba(0, 0, 0, 0.6);
6
6
  --sp-surface-subtle: #eef1f6;
7
7
  --sp-surface-hero: linear-gradient(135deg, #5b6ee1 0%, #6f3fd7 100%);
8
+ --sp-surface-hover: #eef1f6;
9
+ --sp-surface-selected: #f0f9ff;
10
+ --sp-surface-active: #d9dfeb;
11
+ --sp-surface-divider: #d9dfeb;
8
12
  --sp-text-on-page-default: #141b24;
9
13
  --sp-text-on-page-muted: #4b576a;
10
14
  --sp-text-on-page-subtle: #657287;
@@ -72,6 +76,10 @@
72
76
  --sp-dropdown-item-hover: #eef1f6;
73
77
  --sp-dropdown-item-active: #f0f9ff;
74
78
  --sp-dropdown-item-text: #141b24;
79
+ --sp-link-default: #1f57db;
80
+ --sp-link-hover: #1946b4;
81
+ --sp-link-active: #173b8f;
82
+ --sp-link-visited: #5d28b8;
75
83
  --sp-color-brand-50: #eef4ff;
76
84
  --sp-color-brand-100: #d9e7ff;
77
85
  --sp-color-brand-200: #b9d2ff;
@@ -184,6 +192,8 @@
184
192
  --sp-layout-container-padding-inline-md: 1.5rem;
185
193
  --sp-layout-container-padding-inline-lg: 2rem;
186
194
  --sp-layout-container-max-width: 72rem;
195
+ --sp-layout-container-max-width-prose: 65ch;
196
+ --sp-layout-sidebar-width: 16rem;
187
197
  --sp-border-width-none: 0;
188
198
  --sp-border-width-base: 1px;
189
199
  --sp-border-width-thick: 2px;
@@ -424,6 +434,10 @@
424
434
  --sp-surface-overlay: rgba(0, 0, 0, 0.6);
425
435
  --sp-surface-subtle: #222b38;
426
436
  --sp-surface-hero: linear-gradient(135deg, #5d28b8 0%, #401f75 100%);
437
+ --sp-surface-hover: #374253;
438
+ --sp-surface-selected: #082f49;
439
+ --sp-surface-active: #4b576a;
440
+ --sp-surface-divider: #374253;
427
441
  --sp-text-on-page-default: #f7f8fb;
428
442
  --sp-text-on-page-muted: #b7c1d4;
429
443
  --sp-text-on-page-subtle: #8a96ad;
@@ -491,6 +505,10 @@
491
505
  --sp-dropdown-item-hover: #374253;
492
506
  --sp-dropdown-item-active: #082f49;
493
507
  --sp-dropdown-item-text: #eef1f6;
508
+ --sp-link-default: #1f57db;
509
+ --sp-link-hover: #1946b4;
510
+ --sp-link-active: #173b8f;
511
+ --sp-link-visited: #5d28b8;
494
512
  }
495
513
  @layer base {
496
514
 
@@ -5,6 +5,10 @@
5
5
  --sp-surface-overlay: rgba(0, 0, 0, 0.6);
6
6
  --sp-surface-subtle: #eef1f6;
7
7
  --sp-surface-hero: linear-gradient(135deg, #5b6ee1 0%, #6f3fd7 100%);
8
+ --sp-surface-hover: #eef1f6;
9
+ --sp-surface-selected: #f0f9ff;
10
+ --sp-surface-active: #d9dfeb;
11
+ --sp-surface-divider: #d9dfeb;
8
12
  --sp-text-on-page-default: #141b24;
9
13
  --sp-text-on-page-muted: #4b576a;
10
14
  --sp-text-on-page-subtle: #657287;
@@ -72,6 +76,10 @@
72
76
  --sp-dropdown-item-hover: #eef1f6;
73
77
  --sp-dropdown-item-active: #f0f9ff;
74
78
  --sp-dropdown-item-text: #141b24;
79
+ --sp-link-default: #1f57db;
80
+ --sp-link-hover: #1946b4;
81
+ --sp-link-active: #173b8f;
82
+ --sp-link-visited: #5d28b8;
75
83
  --sp-color-brand-50: #eef4ff;
76
84
  --sp-color-brand-100: #d9e7ff;
77
85
  --sp-color-brand-200: #b9d2ff;
@@ -184,6 +192,8 @@
184
192
  --sp-layout-container-padding-inline-md: 1.5rem;
185
193
  --sp-layout-container-padding-inline-lg: 2rem;
186
194
  --sp-layout-container-max-width: 72rem;
195
+ --sp-layout-container-max-width-prose: 65ch;
196
+ --sp-layout-sidebar-width: 16rem;
187
197
  --sp-border-width-none: 0;
188
198
  --sp-border-width-base: 1px;
189
199
  --sp-border-width-thick: 2px;
@@ -424,6 +434,10 @@
424
434
  --sp-surface-overlay: rgba(0, 0, 0, 0.6);
425
435
  --sp-surface-subtle: #222b38;
426
436
  --sp-surface-hero: linear-gradient(135deg, #5d28b8 0%, #401f75 100%);
437
+ --sp-surface-hover: #374253;
438
+ --sp-surface-selected: #082f49;
439
+ --sp-surface-active: #4b576a;
440
+ --sp-surface-divider: #374253;
427
441
  --sp-text-on-page-default: #f7f8fb;
428
442
  --sp-text-on-page-muted: #b7c1d4;
429
443
  --sp-text-on-page-subtle: #8a96ad;
@@ -491,6 +505,10 @@
491
505
  --sp-dropdown-item-hover: #374253;
492
506
  --sp-dropdown-item-active: #082f49;
493
507
  --sp-dropdown-item-text: #eef1f6;
508
+ --sp-link-default: #1f57db;
509
+ --sp-link-hover: #1946b4;
510
+ --sp-link-active: #173b8f;
511
+ --sp-link-visited: #5d28b8;
494
512
  }
495
513
  @layer components {
496
514
 
@@ -809,6 +827,23 @@
809
827
  --sp-component-nav-link-active: var(--sp-nav-link-active);
810
828
  --sp-component-nav-border: var(--sp-nav-border);
811
829
 
830
+ /* sidebar roles (reuses nav roles - vertical counterpart to the top-bar nav) */
831
+ --sp-component-sidebar-bg: var(--sp-nav-bg);
832
+ --sp-component-sidebar-text: var(--sp-nav-text);
833
+ --sp-component-sidebar-link: var(--sp-nav-link);
834
+ --sp-component-sidebar-link-hover: var(--sp-nav-link-hover);
835
+ --sp-component-sidebar-link-active: var(--sp-nav-link-active);
836
+ --sp-component-sidebar-border: var(--sp-nav-border);
837
+ --sp-component-sidebar-width: var(--sp-layout-sidebar-width);
838
+ --sp-component-sidebar-z-index: var(--sp-z-index-fixed);
839
+ --sp-component-sidebar-backdrop: var(--sp-surface-overlay);
840
+ --sp-component-sidebar-backdrop-z-index: var(--sp-z-index-overlay);
841
+
842
+ /* footer roles (reuses nav roles - bottom-bar counterpart to the top-bar nav) */
843
+ --sp-component-footer-bg: var(--sp-nav-bg);
844
+ --sp-component-footer-text: var(--sp-nav-text);
845
+ --sp-component-footer-border: var(--sp-nav-border);
846
+
812
847
  /* toast roles */
813
848
  --sp-component-toast-radius: var(--sp-radius-lg);
814
849
  --sp-component-toast-padding-x: var(--sp-space-16);
@@ -2722,6 +2757,120 @@
2722
2757
  opacity: var(--sp-opacity-disabled);
2723
2758
  }
2724
2759
 
2760
+ /* SIDEBAR -------------------------------------------------------------- */
2761
+ .sp-sidebar {
2762
+ display: flex;
2763
+ flex-direction: column;
2764
+ gap: var(--sp-space-16);
2765
+ width: var(--sp-component-sidebar-width);
2766
+ padding: var(--sp-space-16);
2767
+ background-color: var(--sp-component-sidebar-bg);
2768
+ color: var(--sp-component-sidebar-text);
2769
+ font-family: var(--sp-font-family-sans);
2770
+ position: fixed;
2771
+ top: 0;
2772
+ left: 0;
2773
+ height: 100%;
2774
+ transform: translateX(-100%);
2775
+ z-index: var(--sp-component-sidebar-z-index);
2776
+ transition:
2777
+ background-color var(--sp-duration-fast) var(--sp-easing-out),
2778
+ color var(--sp-duration-fast) var(--sp-easing-out),
2779
+ border-color var(--sp-duration-fast) var(--sp-easing-out),
2780
+ transform var(--sp-duration-fast) var(--sp-easing-out);
2781
+ }
2782
+
2783
+ .sp-sidebar--bordered {
2784
+ border-right: var(--sp-component-border-width) solid var(--sp-component-sidebar-border);
2785
+ }
2786
+
2787
+ .sp-sidebar__link {
2788
+ color: var(--sp-component-sidebar-link);
2789
+ font-size: var(--sp-font-sm-size);
2790
+ line-height: var(--sp-font-sm-line-height);
2791
+ font-weight: var(--sp-font-sm-weight);
2792
+ text-decoration: none;
2793
+ transition: color var(--sp-duration-fast) var(--sp-easing-out);
2794
+ }
2795
+
2796
+ .sp-sidebar__link:hover,
2797
+ .sp-sidebar__link--hover,
2798
+ .sp-sidebar__link.is-hover {
2799
+ color: var(--sp-component-sidebar-link-hover);
2800
+ }
2801
+
2802
+ .sp-sidebar__link:focus-visible,
2803
+ .sp-sidebar__link--focus,
2804
+ .sp-sidebar__link.is-focus {
2805
+ outline: none;
2806
+ box-shadow: 0 0 0 calc(var(--sp-focus-ring-width) + var(--sp-component-border-width)) var(--sp-color-focus-primary);
2807
+ }
2808
+
2809
+ .sp-sidebar__link--active,
2810
+ .sp-sidebar__link.is-active {
2811
+ color: var(--sp-component-sidebar-link-active);
2812
+ }
2813
+
2814
+ .sp-sidebar__link--disabled,
2815
+ .sp-sidebar__link[aria-disabled="true"] {
2816
+ pointer-events: none;
2817
+ opacity: var(--sp-opacity-disabled);
2818
+ }
2819
+
2820
+ .sp-sidebar-backdrop {
2821
+ display: none;
2822
+ position: fixed;
2823
+ inset: 0;
2824
+ background-color: var(--sp-component-sidebar-backdrop);
2825
+ z-index: var(--sp-component-sidebar-backdrop-z-index);
2826
+ }
2827
+
2828
+ [data-sidebar-open="true"] .sp-sidebar {
2829
+ transform: translateX(0);
2830
+ }
2831
+
2832
+ [data-sidebar-open="true"] .sp-sidebar-backdrop {
2833
+ display: block;
2834
+ }
2835
+
2836
+ /* Above breakpoints.md, the sidebar docks inline instead of as an off-canvas
2837
+ drawer - see Grid's @media literal convention for why this is a literal. */
2838
+ @media (min-width: 768px) {
2839
+ .sp-sidebar {
2840
+ position: static;
2841
+ height: auto;
2842
+ transform: none;
2843
+ }
2844
+
2845
+ .sp-sidebar-backdrop {
2846
+ display: none;
2847
+ }
2848
+ }
2849
+
2850
+ /* FOOTER ----------------------------------------------------------------- */
2851
+ .sp-footer {
2852
+ display: flex;
2853
+ align-items: center;
2854
+ justify-content: space-between;
2855
+ gap: var(--sp-space-16);
2856
+ padding: var(--sp-space-12) var(--sp-space-16);
2857
+ background-color: var(--sp-component-footer-bg);
2858
+ color: var(--sp-component-footer-text);
2859
+ font-family: var(--sp-font-family-sans);
2860
+ transition:
2861
+ background-color var(--sp-duration-fast) var(--sp-easing-out),
2862
+ color var(--sp-duration-fast) var(--sp-easing-out),
2863
+ border-color var(--sp-duration-fast) var(--sp-easing-out);
2864
+ }
2865
+
2866
+ .sp-footer--bordered {
2867
+ border-top: var(--sp-component-border-width) solid var(--sp-component-footer-border);
2868
+ }
2869
+
2870
+ .sp-footer--full {
2871
+ width: 100%;
2872
+ }
2873
+
2725
2874
  /* TOASTS ----------------------------------------------------------------- */
2726
2875
  .sp-toast {
2727
2876
  display: flex;
package/dist/index.cjs CHANGED
@@ -788,6 +788,36 @@ function getNavLinkClasses(opts = {}) {
788
788
  );
789
789
  }
790
790
 
791
+ // src/recipes/sidebar.ts
792
+ function getSidebarClasses(opts = {}) {
793
+ const { bordered = false } = opts;
794
+ return cx("sp-sidebar", bordered && "sp-sidebar--bordered");
795
+ }
796
+ function getSidebarLinkClasses(opts = {}) {
797
+ const {
798
+ active = false,
799
+ disabled = false,
800
+ hovered = false,
801
+ focused = false
802
+ } = opts;
803
+ return cx(
804
+ "sp-sidebar__link",
805
+ active && "sp-sidebar__link--active",
806
+ disabled && "sp-sidebar__link--disabled",
807
+ hovered && "sp-sidebar__link--hover is-hover",
808
+ focused && "sp-sidebar__link--focus is-focus"
809
+ );
810
+ }
811
+ function getSidebarBackdropClasses() {
812
+ return cx("sp-sidebar-backdrop");
813
+ }
814
+
815
+ // src/recipes/footer.ts
816
+ function getFooterClasses(opts = {}) {
817
+ const { bordered = false, fullWidth = false } = opts;
818
+ return cx("sp-footer", bordered && "sp-footer--bordered", fullWidth && "sp-footer--full");
819
+ }
820
+
791
821
  // src/recipes/toast.ts
792
822
  var TOAST_VARIANTS = {
793
823
  info: true,
@@ -899,8 +929,19 @@ function getModalClasses(opts = {}) {
899
929
  }
900
930
 
901
931
  // src/recipes/container.ts
902
- function getContainerClasses(_opts = {}) {
903
- return "sp-container";
932
+ var CONTAINER_MAX_WIDTHS = {
933
+ none: true,
934
+ prose: true
935
+ };
936
+ function getContainerClasses(opts = {}) {
937
+ const { maxWidth: maxWidthInput } = opts;
938
+ const maxWidth = resolveOption({
939
+ name: "container maxWidth",
940
+ value: maxWidthInput,
941
+ allowed: CONTAINER_MAX_WIDTHS,
942
+ fallback: "none"
943
+ });
944
+ return cx("sp-container", maxWidth !== "none" && `sp-container--max-width-${maxWidth}`);
904
945
  }
905
946
 
906
947
  // src/recipes/stack.ts
@@ -908,15 +949,28 @@ var STACK_DIRECTIONS = {
908
949
  vertical: true,
909
950
  horizontal: true
910
951
  };
952
+ var STACK_BASES = {
953
+ none: true,
954
+ sidebar: true
955
+ };
911
956
  function getStackClasses(opts = {}) {
912
- const { direction: directionInput } = opts;
957
+ const { direction: directionInput, basis: basisInput } = opts;
913
958
  const direction = resolveOption({
914
959
  name: "stack direction",
915
960
  value: directionInput,
916
961
  allowed: STACK_DIRECTIONS,
917
962
  fallback: "vertical"
918
963
  });
919
- return direction === "horizontal" ? "sp-hstack" : "sp-stack";
964
+ const basis = resolveOption({
965
+ name: "stack basis",
966
+ value: basisInput,
967
+ allowed: STACK_BASES,
968
+ fallback: "none"
969
+ });
970
+ return cx(
971
+ direction === "horizontal" ? "sp-hstack" : "sp-stack",
972
+ basis !== "none" && `sp-stack--basis-${basis}`
973
+ );
920
974
  }
921
975
 
922
976
  // src/recipes/section.ts
@@ -964,6 +1018,7 @@ exports.getContainerClasses = getContainerClasses;
964
1018
  exports.getDropdownClasses = getDropdownClasses;
965
1019
  exports.getDropdownItemClasses = getDropdownItemClasses;
966
1020
  exports.getDropdownMenuClasses = getDropdownMenuClasses;
1021
+ exports.getFooterClasses = getFooterClasses;
967
1022
  exports.getGridClasses = getGridClasses;
968
1023
  exports.getIconBoxClasses = getIconBoxClasses;
969
1024
  exports.getInputClasses = getInputClasses;
@@ -986,6 +1041,9 @@ exports.getRatingStarClasses = getRatingStarClasses;
986
1041
  exports.getRatingStarsClasses = getRatingStarsClasses;
987
1042
  exports.getRatingTextClasses = getRatingTextClasses;
988
1043
  exports.getSectionClasses = getSectionClasses;
1044
+ exports.getSidebarBackdropClasses = getSidebarBackdropClasses;
1045
+ exports.getSidebarClasses = getSidebarClasses;
1046
+ exports.getSidebarLinkClasses = getSidebarLinkClasses;
989
1047
  exports.getSpinnerClasses = getSpinnerClasses;
990
1048
  exports.getStackClasses = getStackClasses;
991
1049
  exports.getTagClasses = getTagClasses;