@cawalch/porchlight 0.1.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 (47) hide show
  1. package/README.md +28 -0
  2. package/dist/porchlight.css +3765 -0
  3. package/dist/porchlight.min.css +1 -0
  4. package/package.json +59 -0
  5. package/porchlight.css +62 -0
  6. package/src/00-layer-order.css +22 -0
  7. package/src/01-reset.css +53 -0
  8. package/src/02-tokens.css +254 -0
  9. package/src/03-themes.css +79 -0
  10. package/src/04-base.css +78 -0
  11. package/src/05-layout.css +209 -0
  12. package/src/06-components/accordion.css +161 -0
  13. package/src/06-components/alert.css +102 -0
  14. package/src/06-components/avatar.css +112 -0
  15. package/src/06-components/badge.css +73 -0
  16. package/src/06-components/breadcrumb.css +111 -0
  17. package/src/06-components/button.css +180 -0
  18. package/src/06-components/card.css +186 -0
  19. package/src/06-components/chip.css +146 -0
  20. package/src/06-components/command-palette.css +201 -0
  21. package/src/06-components/data-table.css +380 -0
  22. package/src/06-components/dialog.css +148 -0
  23. package/src/06-components/drawer.css +137 -0
  24. package/src/06-components/dropdown.css +180 -0
  25. package/src/06-components/empty-state.css +85 -0
  26. package/src/06-components/field.css +125 -0
  27. package/src/06-components/file-upload.css +104 -0
  28. package/src/06-components/nav.css +185 -0
  29. package/src/06-components/pagination.css +106 -0
  30. package/src/06-components/popover-menu.css +146 -0
  31. package/src/06-components/progress.css +77 -0
  32. package/src/06-components/reveal.css +73 -0
  33. package/src/06-components/scroll-progress.css +73 -0
  34. package/src/06-components/segmented.css +113 -0
  35. package/src/06-components/skeleton.css +73 -0
  36. package/src/06-components/stat.css +107 -0
  37. package/src/06-components/stepper.css +172 -0
  38. package/src/06-components/switch.css +138 -0
  39. package/src/06-components/tabs.css +164 -0
  40. package/src/06-components/tag-input.css +77 -0
  41. package/src/06-components/textarea-auto.css +77 -0
  42. package/src/06-components/timeline.css +129 -0
  43. package/src/06-components/toast.css +175 -0
  44. package/src/06-components/toolbar.css +87 -0
  45. package/src/06-components/tooltip.css +104 -0
  46. package/src/07-utilities.css +77 -0
  47. package/src/08-enhancements.css +129 -0
@@ -0,0 +1,106 @@
1
+ /*
2
+ * Porchlight - pagination component
3
+ * ===========================================================================
4
+ * Navigation control for paginated data - the bar below a table or list.
5
+ * Previous / Next buttons flank a row of page number buttons. A compact
6
+ * [data-size="sm"] variant shrinks the controls for embedded use (inside a
7
+ * toolbar or table footer).
8
+ *
9
+ * Sizing comes from --pl-control-* tokens (like .c-button), so density and
10
+ * touch-target rules apply automatically. The active page uses aria-current,
11
+ * which is the semantic attribute for "you are here" navigation.
12
+ *
13
+ * The page buttons reuse .c-button[data-variant="ghost"] styling conventions
14
+ * (accent wash for the active state) but are self-contained so pagination works
15
+ * without also loading .c-button. If .c-button IS present, a .c-button inside
16
+ * the toolbar inherits its own styling - the two coexist without conflict.
17
+ */
18
+ @layer porchlight.components {
19
+ @scope (.c-pagination) {
20
+ :scope {
21
+ --c-pagination-min: var(--pl-control-block-size);
22
+
23
+ display: flex;
24
+ align-items: center;
25
+ gap: var(--pl-space-1);
26
+ }
27
+
28
+ /* A page button - square-ish, ghost-like. */
29
+ .c-pagination__page,
30
+ .c-pagination__nav {
31
+ appearance: none;
32
+ display: inline-flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ gap: var(--pl-space-1);
36
+ min-block-size: var(--c-pagination-min);
37
+ min-inline-size: var(--c-pagination-min);
38
+ padding-inline: var(--pl-space-2);
39
+ border: 1px solid transparent;
40
+ border-radius: var(--pl-radius-md);
41
+ background: transparent;
42
+ color: var(--pl-color-text-muted);
43
+ font: inherit;
44
+ font-weight: var(--pl-font-weight-medium);
45
+ cursor: pointer;
46
+ transition:
47
+ background-color var(--pl-duration-1) var(--pl-ease-standard),
48
+ color var(--pl-duration-1) var(--pl-ease-standard);
49
+ }
50
+
51
+ /* Active page - aria-current="page" marks "you are here". */
52
+ .c-pagination__page[aria-current="page"] {
53
+ background: var(--pl-color-accent);
54
+ color: var(--pl-color-accent-text);
55
+ font-weight: var(--pl-font-weight-semibold);
56
+ }
57
+
58
+ /* Compact variant - smaller targets for embedded use (table footers). */
59
+ :scope[data-size="sm"] {
60
+ --c-pagination-min: 2rem;
61
+ }
62
+
63
+ .c-pagination__page:disabled,
64
+ .c-pagination__nav:disabled {
65
+ opacity: var(--pl-opacity-disabled);
66
+ cursor: not-allowed;
67
+ }
68
+
69
+ /* Ellipsis - a non-interactive spacer for skipped pages. */
70
+ .c-pagination__ellipsis {
71
+ display: inline-flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ min-inline-size: var(--c-pagination-min);
75
+ color: var(--pl-color-text-muted);
76
+ user-select: none;
77
+ }
78
+ }
79
+
80
+ /* Live-state hover OUTSIDE @scope. */
81
+ :where(.c-pagination__page:not([aria-current="page"])):is(
82
+ :hover,
83
+ :focus-visible
84
+ ) {
85
+ background: var(--pl-color-surface-2);
86
+ color: var(--pl-color-text);
87
+ }
88
+
89
+ :where(.c-pagination__page):focus-visible,
90
+ :where(.c-pagination__nav):focus-visible {
91
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
92
+ outline-offset: calc(var(--pl-focus-offset) * -1);
93
+ }
94
+
95
+ @media (forced-colors: active) {
96
+ :where(.c-pagination__page)[aria-current="page"] {
97
+ background: Highlight;
98
+ color: HighlightText;
99
+ }
100
+
101
+ :where(.c-pagination__page):focus-visible,
102
+ :where(.c-pagination__nav):focus-visible {
103
+ outline-color: Highlight;
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,146 @@
1
+ /*
2
+ * Porchlight - popover menu component
3
+ * ===========================================================================
4
+ * A dropdown menu anchored to its trigger - account menus, action menus,
5
+ * context menus. Uses the native Popover API (top-layer, light-dismiss,
6
+ * focus management) + CSS anchor positioning (tethered to the trigger) +
7
+ * @starting-style for a native enter/exit animation.
8
+ *
9
+ * The popover element MUST have the `popover` attribute in HTML:
10
+ * <div popover class="c-menu__popover" id="...">...</div>
11
+ * The trigger uses `popovertarget="id"` to open it declaratively - no JS.
12
+ *
13
+ * Anchor positioning is @supports-gated: browsers without it fall back to the
14
+ * UA's default centering; Chrome 149+ positions the popover at the trigger's
15
+ * block-end / inline-end corner via position-area.
16
+ *
17
+ * Multiple menus on one page need unique anchor names - set --c-menu-anchor
18
+ * per instance (defaults to --pl-menu-anchor; works for a single menu).
19
+ */
20
+ @layer porchlight.components {
21
+ @scope (.c-menu) {
22
+ :scope {
23
+ --c-menu-anchor: --pl-menu-anchor;
24
+
25
+ display: inline-block;
26
+ }
27
+
28
+ .c-menu__trigger {
29
+ anchor-name: var(--c-menu-anchor);
30
+ }
31
+
32
+ .c-menu__popover {
33
+ margin: 0;
34
+ padding: var(--pl-space-2);
35
+ min-inline-size: 12rem;
36
+
37
+ /* Floating surface: softened border + Liquid Glass. */
38
+ border: 1px solid color-mix(
39
+ in oklab,
40
+ var(--pl-color-border),
41
+ transparent 30%
42
+ );
43
+ border-radius: var(--pl-radius-xl);
44
+ background: light-dark(
45
+ oklch(100% 0 0deg / 96%),
46
+ oklch(17% 0.04 250deg / 94%)
47
+ );
48
+ backdrop-filter: blur(var(--pl-backdrop-blur)) saturate(var(--pl-backdrop-saturate));
49
+ -webkit-backdrop-filter: blur(var(--pl-backdrop-blur)) saturate(var(--pl-backdrop-saturate));
50
+ color: var(--pl-color-text);
51
+ box-shadow: var(--pl-shadow-3);
52
+
53
+ /* Asymmetric motion: enters deliberately, exits snappily. */
54
+ opacity: 0;
55
+ transform: translateY(-4px);
56
+ transition:
57
+ opacity var(--pl-duration-enter) var(--pl-ease-decelerate),
58
+ transform var(--pl-duration-enter) var(--pl-ease-decelerate),
59
+ overlay var(--pl-duration-exit) var(--pl-ease-accelerate) allow-discrete,
60
+ display var(--pl-duration-exit) var(--pl-ease-accelerate) allow-discrete;
61
+ }
62
+
63
+ /* Anchor positioning - tether the popover to the trigger. @supports-gated
64
+ so browsers without anchor support fall back gracefully. inset: auto
65
+ resets the UA's default centering for popovers. */
66
+ @supports (position-anchor: --x) {
67
+ .c-menu__popover {
68
+ inset: auto;
69
+ position: absolute;
70
+ position-anchor: var(--c-menu-anchor);
71
+ position-area: block-end span-inline-end;
72
+ position-try-fallbacks: flip-block;
73
+ margin-block-start: var(--pl-space-2);
74
+ }
75
+ }
76
+
77
+ /* Open state: visible + settled. */
78
+ .c-menu__popover:popover-open {
79
+ opacity: 1;
80
+ transform: translateY(0);
81
+ }
82
+
83
+ /* Enter: @starting-style sets the initial state BEFORE the open transition. */
84
+ @starting-style {
85
+ .c-menu__popover:popover-open {
86
+ opacity: 0;
87
+ transform: translateY(-4px);
88
+ }
89
+ }
90
+
91
+ .c-menu__divider {
92
+ margin-block: var(--pl-space-1);
93
+ border: 0;
94
+ border-block-start: 1px solid var(--pl-color-border);
95
+ }
96
+ }
97
+
98
+ /* Menu items - OUTSIDE @scope (live-state pseudos :hover/:focus-visible on
99
+ descendants don't reliably apply inside @scope; same lesson as .c-field).
100
+ Each item is a full-width <a> or <button> for keyboard access. */
101
+ :where(.c-menu__popover) > :is(a, button) {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: var(--pl-space-2);
105
+ inline-size: 100%;
106
+ min-block-size: 2.25rem;
107
+ padding-inline: var(--pl-space-3);
108
+ border: 0;
109
+ border-radius: var(--pl-radius-sm);
110
+ background: transparent;
111
+ color: inherit;
112
+ font: inherit;
113
+ text-align: start;
114
+ text-decoration: none;
115
+ cursor: pointer;
116
+ }
117
+
118
+ :where(.c-menu__popover) > :is(a, button):is(:hover, :focus-visible) {
119
+ background: var(--pl-color-surface-2);
120
+ }
121
+
122
+ :where(.c-menu__popover) > :is(a, button):focus-visible {
123
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
124
+ outline-offset: calc(var(--pl-focus-offset) * -1);
125
+ }
126
+
127
+ /* Destructive item - sign out, delete. */
128
+ :where(.c-menu__popover) > :is(a, button)[data-tone="danger"] {
129
+ color: var(--pl-color-danger-text);
130
+ }
131
+
132
+ :where(.c-menu__popover)
133
+ > :is(a, button)[data-tone="danger"]:is(:hover, :focus-visible) {
134
+ background: var(--pl-color-danger-bg);
135
+ }
136
+
137
+ @media (forced-colors: active) {
138
+ :where(.c-menu__popover) {
139
+ border-color: ButtonBorder;
140
+ }
141
+
142
+ :where(.c-menu__popover) > :is(a, button):focus-visible {
143
+ outline-color: Highlight;
144
+ }
145
+ }
146
+ }
@@ -0,0 +1,77 @@
1
+ /*
2
+ * Porchlight - progress bar component
3
+ * ===========================================================================
4
+ * A linear progress indicator for task completion, uploads, or capacity.
5
+ * Determinate ([data-value="65"]) shows a fill proportional to the value;
6
+ * indeterminate ([data-indeterminate]) shows an animated sweep.
7
+ *
8
+ * Tone variants via [data-tone]: default (accent), success, warning, danger.
9
+ * For multi-segment progress (e.g. storage usage), nest multiple
10
+ * .c-progress__bar elements with different tones.
11
+ *
12
+ * The track is a 4px-high rounded bar; the fill animates width via
13
+ * transition (respects reduced-motion). The indeterminate animation uses
14
+ * a keyframe sweep that stops under reduced-motion (themes layer clamp).
15
+ */
16
+ @layer porchlight.components {
17
+ @scope (.c-progress) {
18
+ :scope {
19
+ --c-progress-tone: var(--pl-color-accent);
20
+ --c-progress-height: 0.375rem;
21
+
22
+ position: relative;
23
+ overflow: hidden;
24
+ block-size: var(--c-progress-height);
25
+ border-radius: var(--pl-radius-pill);
26
+ background: var(--pl-color-surface-2);
27
+ }
28
+
29
+ .c-progress__bar {
30
+ block-size: 100%;
31
+ border-radius: var(--pl-radius-pill);
32
+ background: var(--c-progress-tone);
33
+ transition: inline-size var(--pl-duration-3) var(--pl-ease-standard);
34
+ }
35
+
36
+ /* Tone variants. */
37
+ :scope[data-tone="success"] {
38
+ --c-progress-tone: var(--pl-color-success);
39
+ }
40
+
41
+ :scope[data-tone="warning"] {
42
+ --c-progress-tone: var(--pl-color-warning);
43
+ }
44
+
45
+ :scope[data-tone="danger"] {
46
+ --c-progress-tone: var(--pl-color-danger);
47
+ }
48
+
49
+ /* Indeterminate - an animated sweep that loops. */
50
+ :scope[data-indeterminate] .c-progress__bar {
51
+ position: absolute;
52
+ inset-block: 0;
53
+ inline-size: 40%;
54
+ animation: c-progress-sweep 1.4s var(--pl-ease-standard) infinite;
55
+ }
56
+ }
57
+
58
+ @keyframes c-progress-sweep {
59
+ 0% {
60
+ inset-inline-start: -40%;
61
+ }
62
+
63
+ 100% {
64
+ inset-inline-start: 100%;
65
+ }
66
+ }
67
+
68
+ @media (forced-colors: active) {
69
+ :where(.c-progress) {
70
+ background: CanvasText;
71
+ }
72
+
73
+ :where(.c-progress__bar) {
74
+ background: Highlight;
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,73 @@
1
+ /*
2
+ * Porchlight - scroll reveal component
3
+ * ===========================================================================
4
+ * Elements that fade/slide in as they enter the viewport. Uses scroll-driven
5
+ * animations (animation-timeline: view()) - zero JavaScript, no
6
+ * IntersectionObserver.
7
+ *
8
+ * @supports-gated: where scroll-driven animations are not supported, elements
9
+ * are always visible (no invisible content).
10
+ *
11
+ * Structure:
12
+ * <div class="c-reveal">Content that fades in on scroll</div>
13
+ *
14
+ * The reveal triggers as the element enters the scrollport, completing when
15
+ * 30% of it is covered. Use on sections, cards, or images below the fold.
16
+ *
17
+ * For varied timing, add [data-delay="1"|"2"|"3"] which shifts the
18
+ * animation-range start.
19
+ */
20
+ @layer porchlight.components {
21
+ /* Scroll-driven reveal. */
22
+ @supports (animation-timeline: view()) {
23
+ :where(.c-reveal) {
24
+ animation: c-reveal-enter both linear;
25
+ animation-timeline: view();
26
+ animation-range: entry 0% cover 30%;
27
+ }
28
+
29
+ /* Delay variants: shift the animation range start later. */
30
+ :where(.c-reveal[data-delay="1"]) {
31
+ animation-range: entry 10% cover 35%;
32
+ }
33
+
34
+ :where(.c-reveal[data-delay="2"]) {
35
+ animation-range: entry 20% cover 40%;
36
+ }
37
+
38
+ :where(.c-reveal[data-delay="3"]) {
39
+ animation-range: entry 30% cover 45%;
40
+ }
41
+ }
42
+
43
+ /* Without scroll-driven animation support, elements are always visible. */
44
+ @supports not (animation-timeline: view()) {
45
+ :where(.c-reveal) {
46
+ animation: none;
47
+ opacity: 1;
48
+ transform: none;
49
+ }
50
+ }
51
+
52
+ /* Under forced colors, ensure content is always visible (no scroll-gated
53
+ animations that could leave content invisible in high-contrast mode). */
54
+ @media (forced-colors: active) {
55
+ :where(.c-reveal) {
56
+ animation: none;
57
+ opacity: 1;
58
+ transform: none;
59
+ }
60
+ }
61
+
62
+ @keyframes c-reveal-enter {
63
+ from {
64
+ opacity: 0;
65
+ transform: translateY(1.5rem);
66
+ }
67
+
68
+ to {
69
+ opacity: 1;
70
+ transform: translateY(0);
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,73 @@
1
+ /*
2
+ * Porchlight - scroll progress bar component
3
+ * ===========================================================================
4
+ * A fixed bar at the viewport top that fills as the user scrolls the page.
5
+ * Uses scroll-driven animations (animation-timeline: scroll()) - zero
6
+ * JavaScript for the progress tracking.
7
+ *
8
+ * @supports-gated: where scroll-driven animations are not supported, the bar
9
+ * is hidden entirely (no misleading partial fill).
10
+ *
11
+ * Structure:
12
+ * <div class="c-scroll-progress" data-tone="accent"></div>
13
+ *
14
+ * Place at the top of the page (fixed position). Tone variants via [data-tone].
15
+ * For per-section progress, set animation-timeline to a named scroll
16
+ * container instead of the root.
17
+ */
18
+ @layer porchlight.components {
19
+ /* Scroll-driven progress bar. */
20
+ @supports (animation-timeline: scroll()) {
21
+ :where(.c-scroll-progress) {
22
+ --c-progress-tone: var(--pl-color-accent);
23
+
24
+ position: fixed;
25
+ inset-block-start: 0;
26
+ inset-inline: 0;
27
+ z-index: var(--pl-z-sticky);
28
+ block-size: 4px;
29
+ transform-origin: inline-start;
30
+ background: var(--c-progress-tone);
31
+
32
+ /* Animate scaleX from 0 to 1 as the page scrolls. */
33
+ animation: c-scroll-progress-grow auto linear;
34
+ animation-timeline: scroll(root);
35
+ }
36
+
37
+ :where(.c-scroll-progress[data-tone="success"]) {
38
+ --c-progress-tone: var(--pl-color-success);
39
+ }
40
+
41
+ :where(.c-scroll-progress[data-tone="warning"]) {
42
+ --c-progress-tone: var(--pl-color-warning);
43
+ }
44
+
45
+ :where(.c-scroll-progress[data-tone="danger"]) {
46
+ --c-progress-tone: var(--pl-color-danger);
47
+ }
48
+ }
49
+
50
+ /* Without scroll-driven animation support, hide the bar. */
51
+ @supports not (animation-timeline: scroll()) {
52
+ :where(.c-scroll-progress) {
53
+ display: none;
54
+ }
55
+ }
56
+
57
+ /* Under forced colors, use the system highlight color for the bar. */
58
+ @media (forced-colors: active) {
59
+ :where(.c-scroll-progress) {
60
+ background: Highlight;
61
+ }
62
+ }
63
+
64
+ @keyframes c-scroll-progress-grow {
65
+ from {
66
+ transform: scaleX(0);
67
+ }
68
+
69
+ to {
70
+ transform: scaleX(1);
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,113 @@
1
+ /*
2
+ * Porchlight - segmented control component
3
+ * ===========================================================================
4
+ * A compact inline switcher for binary/ternary toggles (list/grid/table view,
5
+ * day/week/month). Built on native radio inputs - CSS-only, keyboard
6
+ * accessible, no JavaScript.
7
+ *
8
+ * The active segment gets an accent background driven by :has(:checked).
9
+ * The control is a pill-shaped container with segments as flex children.
10
+ *
11
+ * Structure:
12
+ * <div class="c-segmented" role="radiogroup" aria-label="View mode">
13
+ * <label class="c-segmented__item">
14
+ * <input type="radio" name="view" checked />
15
+ * <span>List</span>
16
+ * </label>
17
+ * <label class="c-segmented__item">
18
+ * <input type="radio" name="view" />
19
+ * <span>Grid</span>
20
+ * </label>
21
+ * </div>
22
+ */
23
+ @layer porchlight.components {
24
+ @scope (.c-segmented) {
25
+ :scope {
26
+ --c-segmented-pad: 0.125rem;
27
+ --c-segmented-gap: var(--pl-space-1);
28
+ --c-segmented-radius: var(--pl-radius-pill);
29
+
30
+ display: inline-flex;
31
+ gap: var(--c-segmented-gap);
32
+ padding: var(--c-segmented-pad);
33
+ border: 1px solid var(--pl-color-border);
34
+ border-radius: var(--c-segmented-radius);
35
+ background: var(--pl-color-surface-2);
36
+ }
37
+
38
+ .c-segmented__item {
39
+ position: relative;
40
+ display: inline-flex;
41
+ align-items: center;
42
+ justify-content: center;
43
+ }
44
+
45
+ /* Visually hide the native radio. */
46
+ .c-segmented__item input {
47
+ position: absolute;
48
+ inline-size: 1px;
49
+ block-size: 1px;
50
+ padding: 0;
51
+ margin: -1px;
52
+ overflow: hidden;
53
+ clip: rect(0, 0, 0, 0);
54
+ white-space: nowrap;
55
+ border: 0;
56
+ }
57
+
58
+ /* The visible label/pill. */
59
+ .c-segmented__item span {
60
+ display: inline-flex;
61
+ align-items: center;
62
+ gap: var(--pl-space-1);
63
+ padding: var(--pl-space-1) var(--pl-space-3);
64
+ border-radius: var(--c-segmented-radius);
65
+ color: var(--pl-color-text-muted);
66
+ font-size: var(--pl-text-sm);
67
+ font-weight: var(--pl-font-weight-medium);
68
+ cursor: pointer;
69
+ user-select: none;
70
+ white-space: nowrap;
71
+ transition:
72
+ background-color var(--pl-duration-1) var(--pl-ease-standard),
73
+ color var(--pl-duration-1) var(--pl-ease-standard);
74
+ }
75
+
76
+ /* Active segment: accent fill. */
77
+ .c-segmented__item:has(input:checked) span {
78
+ background: var(--pl-color-surface);
79
+ color: var(--pl-color-accent);
80
+ font-weight: var(--pl-font-weight-semibold);
81
+ box-shadow: var(--pl-shadow-1);
82
+ }
83
+
84
+ /* Hover on non-checked. */
85
+ .c-segmented__item:not(:has(input:checked), :has(input:disabled))
86
+ span:hover {
87
+ color: var(--pl-color-text);
88
+ }
89
+
90
+ /* Focus ring. */
91
+ .c-segmented__item:has(input:focus-visible) span {
92
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
93
+ outline-offset: var(--pl-focus-offset);
94
+ }
95
+
96
+ /* Disabled. */
97
+ .c-segmented__item:has(input:disabled) span {
98
+ opacity: var(--pl-opacity-disabled);
99
+ cursor: not-allowed;
100
+ }
101
+ }
102
+
103
+ @media (forced-colors: active) {
104
+ :where(.c-segmented) {
105
+ border-color: ButtonBorder;
106
+ }
107
+
108
+ :where(.c-segmented__item:has(input:checked) span) {
109
+ background: Highlight;
110
+ color: HighlightText;
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,73 @@
1
+ /*
2
+ * Porchlight - skeleton component
3
+ * ===========================================================================
4
+ * Loading placeholders that mimic the shape of real content. A pulsing shimmer
5
+ * communicates "something is loading" without a spinner (which implies an
6
+ * indeterminate wait). Used in cards, lists, and tables while data fetches.
7
+ *
8
+ * Three shapes via [data-shape]: "text" (a line, default), "circle" (an
9
+ * avatar), and "rect" (a block). Width is controlled by --c-skeleton-w for
10
+ * inline sizing; text rows default to 100%.
11
+ *
12
+ * The shimmer is a CSS-only animation (opacity pulse). Under reduced motion
13
+ * the themes layer's universal 1ms clamp stops it; the skeleton remains as a
14
+ * static grey block (still legible as a placeholder).
15
+ *
16
+ * A [data-lines] attribute on a container with .c-skeleton children generates
17
+ * staggered widths for prose-like placeholder stacks - see the preview page.
18
+ */
19
+ @layer porchlight.components {
20
+ @scope (.c-skeleton) {
21
+ :scope {
22
+ --c-skeleton-w: 100%;
23
+
24
+ display: block;
25
+ inline-size: var(--c-skeleton-w);
26
+ block-size: var(--pl-text-md);
27
+ border-radius: var(--pl-radius-sm);
28
+ background: var(--pl-color-surface-2);
29
+ animation: c-skeleton-pulse 1.8s var(--pl-ease-standard) infinite;
30
+ }
31
+
32
+ :scope[data-shape="circle"] {
33
+ inline-size: var(--c-skeleton-w, 2.5rem);
34
+ block-size: var(--c-skeleton-w, 2.5rem);
35
+ border-radius: var(--pl-radius-pill);
36
+ }
37
+
38
+ :scope[data-shape="rect"] {
39
+ block-size: var(--c-skeleton-w, 6rem);
40
+ inline-size: 100%;
41
+ border-radius: var(--pl-radius-md);
42
+ }
43
+
44
+ :scope[data-shape="text"][data-line="sm"] {
45
+ block-size: var(--pl-text-sm);
46
+ }
47
+ }
48
+
49
+ /* A skeleton group - staggered text lines for prose-like placeholders. */
50
+ :where(.c-skeleton-group) {
51
+ display: flex;
52
+ flex-direction: column;
53
+ gap: var(--pl-space-2);
54
+ }
55
+
56
+ @keyframes c-skeleton-pulse {
57
+ 0%,
58
+ 100% {
59
+ opacity: 1;
60
+ }
61
+
62
+ 50% {
63
+ opacity: 0.4;
64
+ }
65
+ }
66
+
67
+ @media (forced-colors: active) {
68
+ :where(.c-skeleton) {
69
+ background: CanvasText;
70
+ opacity: 0.3;
71
+ }
72
+ }
73
+ }