@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,107 @@
1
+ /*
2
+ * Porchlight - stat component
3
+ * ===========================================================================
4
+ * A KPI / metric tile - the workhorse of SaaS dashboards. Displays a single
5
+ * number with a label, an optional unit, a trend indicator (up/down/flat), and
6
+ * an optional sparkline slot. Designed to sit inside .l-grid (auto-fitting
7
+ * dashboard widgets) or inside .c-card bodies.
8
+ *
9
+ * Structure: .c-stat > .c-stat__label + .c-stat__value (+ .c-stat__unit) +
10
+ * .c-stat__trend + .c-stat__spark. The value uses tabular-nums so numbers
11
+ * don't jitter when they update. The trend uses semantic colors + an arrow
12
+ * glyph, not just color (colorblind-safe).
13
+ *
14
+ * This is a display component - no interactive states. It composes with .c-card
15
+ * when you need elevation: place .c-stat inside a .c-card for a raised tile.
16
+ */
17
+ @layer porchlight.components {
18
+ @scope (.c-stat) {
19
+ :scope {
20
+ --c-stat-gap: var(--pl-space-1);
21
+
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: var(--pl-space-2);
25
+ }
26
+
27
+ .c-stat__label {
28
+ font-size: var(--pl-text-sm);
29
+ font-weight: var(--pl-font-weight-semibold);
30
+ color: var(--pl-color-text-muted);
31
+ line-height: 1;
32
+ }
33
+
34
+ .c-stat__value {
35
+ display: flex;
36
+ align-items: baseline;
37
+ gap: var(--pl-space-2);
38
+ font-size: clamp(1.75rem, 1.5rem + 1vi, 2.25rem);
39
+ font-weight: var(--pl-font-weight-bold);
40
+ line-height: var(--pl-leading-tight);
41
+ color: var(--pl-color-text);
42
+ font-variant-numeric: tabular-nums;
43
+ letter-spacing: -0.02em;
44
+ }
45
+
46
+ .c-stat__unit {
47
+ font-size: var(--pl-text-md);
48
+ font-weight: var(--pl-font-weight-medium);
49
+ color: var(--pl-color-text-muted);
50
+ }
51
+
52
+ .c-stat__trend {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ gap: var(--pl-space-1);
56
+ font-size: var(--pl-text-sm);
57
+ font-weight: var(--pl-font-weight-semibold);
58
+ font-variant-numeric: tabular-nums;
59
+ color: var(--pl-color-text-muted);
60
+ }
61
+
62
+ /* Trend directions - semantic colors. The arrow glyph is always present
63
+ so color is not the sole signal (colorblind-safe). */
64
+ .c-stat__trend[data-direction="up"] {
65
+ color: var(--pl-color-success-text);
66
+ }
67
+
68
+ .c-stat__trend[data-direction="down"] {
69
+ color: var(--pl-color-danger-text);
70
+ }
71
+
72
+ /* The arrow lives in a ::before so app HTML doesn't need a glyph icon. */
73
+ .c-stat__trend::before {
74
+ display: inline-block;
75
+ font-size: 0.7em;
76
+ line-height: 1;
77
+ }
78
+
79
+ .c-stat__trend[data-direction="up"]::before {
80
+ content: "▲";
81
+ }
82
+
83
+ .c-stat__trend[data-direction="down"]::before {
84
+ content: "▼";
85
+ }
86
+
87
+ .c-stat__trend[data-direction="flat"]::before {
88
+ content: "▶";
89
+ transform: rotate(-90deg);
90
+ }
91
+
92
+ /* Optional sparkline slot - an inline SVG or canvas. */
93
+ .c-stat__spark {
94
+ margin-block-start: var(--pl-space-1);
95
+ }
96
+ }
97
+
98
+ @media (forced-colors: active) {
99
+ :where(.c-stat__trend)[data-direction="up"] {
100
+ color: CanvasText;
101
+ }
102
+
103
+ :where(.c-stat__trend)[data-direction="down"] {
104
+ color: CanvasText;
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,172 @@
1
+ /*
2
+ * Porchlight - stepper component
3
+ * ===========================================================================
4
+ * A horizontal (default) or vertical (container-query collapse) multi-step
5
+ * indicator. Each step has a visual marker (circle with number or check) and
6
+ * a label. Connectors between steps are CSS-only (no SVG).
7
+ *
8
+ * State is driven by attributes:
9
+ * [data-state="completed"] - done (checkmark, accent fill)
10
+ * [data-state="current"] - active (accent ring, bold label)
11
+ * [data-state="upcoming"] - pending (muted)
12
+ *
13
+ * Structure:
14
+ * <ol class="c-stepper">
15
+ * <li class="c-stepper__step" data-state="completed">
16
+ * <span class="c-stepper__marker">1</span>
17
+ * <span class="c-stepper__label">Account</span>
18
+ * </li>
19
+ * <li class="c-stepper__step" data-state="current">
20
+ * <span class="c-stepper__marker">2</span>
21
+ * <span class="c-stepper__label">Profile</span>
22
+ * </li>
23
+ * <li class="c-stepper__step" data-state="upcoming">
24
+ * <span class="c-stepper__marker">3</span>
25
+ * <span class="c-stepper__label">Confirm</span>
26
+ * </li>
27
+ * </ol>
28
+ *
29
+ * Responsive: collapses to vertical below ~36rem container width.
30
+ */
31
+ @layer porchlight.components {
32
+ @scope (.c-stepper) {
33
+ :scope {
34
+ --c-stepper-marker-size: 2rem;
35
+ --c-stepper-gap: var(--pl-space-4);
36
+
37
+ display: flex;
38
+ align-items: flex-start;
39
+ gap: var(--c-stepper-gap);
40
+ margin: 0;
41
+ padding: 0;
42
+ list-style: none;
43
+ }
44
+
45
+ .c-stepper__step {
46
+ display: flex;
47
+ flex-direction: column;
48
+ align-items: center;
49
+ gap: var(--pl-space-2);
50
+ flex: 1;
51
+ position: relative;
52
+ text-align: center;
53
+
54
+ /* Connector line to the next step. */
55
+ &::before {
56
+ content: "";
57
+ position: absolute;
58
+ inset-block-start: calc(var(--c-stepper-marker-size) / 2);
59
+ inset-inline: calc(
60
+ 50% + var(--c-stepper-marker-size) / 2 + var(--c-stepper-gap) / 2
61
+ )
62
+ calc(
63
+ -50% + var(--c-stepper-marker-size) / 2 + var(--c-stepper-gap) / 2
64
+ );
65
+ block-size: 2px;
66
+ background: var(--pl-color-border);
67
+ z-index: var(--pl-z-base);
68
+ }
69
+
70
+ /* No connector after the last step. */
71
+ &:last-child::before {
72
+ content: none;
73
+ }
74
+ }
75
+
76
+ /* Completed step connector: accent fill. */
77
+ .c-stepper__step[data-state="completed"]::before {
78
+ background: var(--pl-color-accent);
79
+ }
80
+
81
+ .c-stepper__marker {
82
+ display: inline-flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ inline-size: var(--c-stepper-marker-size);
86
+ block-size: var(--c-stepper-marker-size);
87
+ border-radius: 50%;
88
+ border: 2px solid var(--pl-color-border);
89
+ background: var(--pl-color-surface);
90
+ color: var(--pl-color-text-muted);
91
+ font-size: var(--pl-text-sm);
92
+ font-weight: var(--pl-font-weight-semibold);
93
+ z-index: var(--pl-z-raised);
94
+ transition:
95
+ background-color var(--pl-duration-2) var(--pl-ease-standard),
96
+ border-color var(--pl-duration-2) var(--pl-ease-standard),
97
+ color var(--pl-duration-2) var(--pl-ease-standard),
98
+ box-shadow var(--pl-duration-2) var(--pl-ease-standard);
99
+ }
100
+
101
+ /* Completed: accent fill + white text. */
102
+ .c-stepper__step[data-state="completed"] .c-stepper__marker {
103
+ border-color: var(--pl-color-accent);
104
+ background: var(--pl-color-accent);
105
+ color: var(--pl-color-accent-text);
106
+ }
107
+
108
+ /* Current: accent ring, bold. */
109
+ .c-stepper__step[data-state="current"] .c-stepper__marker {
110
+ border-color: var(--pl-color-accent);
111
+ color: var(--pl-color-accent);
112
+ box-shadow: 0 0 0 4px
113
+ color-mix(in oklab, var(--pl-color-accent), transparent var(--pl-focus-glow-opacity));
114
+ }
115
+
116
+ .c-stepper__label {
117
+ font-size: var(--pl-text-sm);
118
+ color: var(--pl-color-text-muted);
119
+ line-height: var(--pl-leading-snug);
120
+ }
121
+
122
+ .c-stepper__step[data-state="current"] .c-stepper__label {
123
+ color: var(--pl-color-text);
124
+ font-weight: var(--pl-font-weight-semibold);
125
+ }
126
+
127
+ /* Container-query collapse to vertical. */
128
+ @container (max-width: 36rem) {
129
+ :scope {
130
+ flex-direction: column;
131
+ gap: 0;
132
+ }
133
+
134
+ .c-stepper__step {
135
+ flex-direction: row;
136
+ align-items: center;
137
+ flex: none;
138
+ text-align: start;
139
+ padding-block-end: var(--pl-space-4);
140
+
141
+ &::before {
142
+ inset-block: var(--c-stepper-marker-size) 0;
143
+ inset-inline: calc(var(--c-stepper-marker-size) / 2) auto;
144
+ inline-size: 2px;
145
+ }
146
+ }
147
+ }
148
+
149
+ /* Container query context (applied outside @scope). */
150
+ }
151
+
152
+ /* Container query context. */
153
+ :where(.c-stepper) {
154
+ container-type: inline-size;
155
+ }
156
+
157
+ @media (forced-colors: active) {
158
+ :where(.c-stepper__marker) {
159
+ border-color: ButtonBorder;
160
+ }
161
+
162
+ :where(.c-stepper__step[data-state="completed"] .c-stepper__marker) {
163
+ border-color: Highlight;
164
+ background: Highlight;
165
+ color: HighlightText;
166
+ }
167
+
168
+ :where(.c-stepper__step[data-state="current"] .c-stepper__marker) {
169
+ border-color: Highlight;
170
+ }
171
+ }
172
+ }
@@ -0,0 +1,138 @@
1
+ /*
2
+ * Porchlight - switch (toggle) component
3
+ * ===========================================================================
4
+ * A CSS-only toggle switch built on a native checkbox. No JavaScript for the
5
+ * toggle itself: <input type="checkbox" role="switch"> + <label>.
6
+ *
7
+ * The track and thumb are styled entirely in CSS. The thumb slides via
8
+ * transition on inset-inline-start. :checked drives the on/off state.
9
+ * RTL-aware: thumb slides in the correct logical direction automatically.
10
+ *
11
+ * Structure:
12
+ * <label class="c-switch">
13
+ * <input type="checkbox" role="switch" class="c-switch__input" />
14
+ * <span class="c-switch__track">
15
+ * <span class="c-switch__thumb"></span>
16
+ * </span>
17
+ * <span class="c-switch__label">Enable feature</span>
18
+ * </label>
19
+ */
20
+ @layer porchlight.components {
21
+ @scope (.c-switch) {
22
+ :scope {
23
+ --c-switch-track-w: 2.75rem;
24
+ --c-switch-track-h: 1.5rem;
25
+ --c-switch-thumb: calc(var(--c-switch-track-h) - 0.25rem);
26
+ --c-switch-gap: var(--pl-space-2);
27
+ --c-switch-on: var(--pl-color-accent);
28
+ --c-switch-off: var(--pl-color-surface-2);
29
+
30
+ display: inline-flex;
31
+ align-items: center;
32
+ gap: var(--c-switch-gap);
33
+ cursor: pointer;
34
+ user-select: none;
35
+ }
36
+
37
+ :scope[disabled],
38
+ :scope:has(.c-switch__input:disabled) {
39
+ opacity: var(--pl-opacity-disabled);
40
+ cursor: not-allowed;
41
+ }
42
+
43
+ /* Visually hide the native checkbox (accessible to AT). */
44
+ .c-switch__input {
45
+ position: absolute;
46
+ inline-size: 1px;
47
+ block-size: 1px;
48
+ padding: 0;
49
+ margin: -1px;
50
+ overflow: hidden;
51
+ clip: rect(0, 0, 0, 0);
52
+ white-space: nowrap;
53
+ border: 0;
54
+ }
55
+
56
+ /* Track: the pill background. */
57
+ .c-switch__track {
58
+ position: relative;
59
+ display: inline-flex;
60
+ align-items: center;
61
+ flex-shrink: 0;
62
+ inline-size: var(--c-switch-track-w);
63
+ block-size: var(--c-switch-track-h);
64
+ border-radius: var(--pl-radius-pill);
65
+ background: var(--c-switch-off);
66
+
67
+ /* No border: surface-2 track provides contrast against page bg.
68
+ forced-colors block below adds ButtonText border for high-contrast mode. */
69
+ transition: background-color var(--pl-duration-2) var(--pl-ease-standard);
70
+ }
71
+
72
+ /* Thumb: the sliding circle. */
73
+ .c-switch__thumb {
74
+ position: absolute;
75
+ inline-size: var(--c-switch-thumb);
76
+ block-size: var(--c-switch-thumb);
77
+ border-radius: 50%;
78
+ background: var(--pl-color-surface);
79
+ box-shadow: var(--pl-shadow-1);
80
+ inset-inline-start: 0.125rem;
81
+ transition: inset-inline-start var(--pl-duration-2)
82
+ var(--pl-ease-standard);
83
+ }
84
+
85
+ /* Checked state: track turns accent, thumb slides to the end. */
86
+ :scope:has(.c-switch__input:checked) .c-switch__track {
87
+ background: var(--c-switch-on);
88
+ border-color: transparent;
89
+ }
90
+
91
+ :scope:has(.c-switch__input:checked) .c-switch__thumb {
92
+ inset-inline-start: calc(
93
+ var(--c-switch-track-w) - var(--c-switch-thumb) - 0.125rem
94
+ );
95
+ }
96
+
97
+ /* Focus ring on the track. */
98
+ :scope:has(.c-switch__input:focus-visible) .c-switch__track {
99
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
100
+ outline-offset: var(--pl-focus-offset);
101
+ }
102
+
103
+ /* Size variants. */
104
+ :scope[data-size="sm"] {
105
+ --c-switch-track-w: 2.25rem;
106
+ --c-switch-track-h: 1.25rem;
107
+ }
108
+
109
+ :scope[data-size="lg"] {
110
+ --c-switch-track-w: 3.25rem;
111
+ --c-switch-track-h: 1.75rem;
112
+ }
113
+
114
+ .c-switch__label {
115
+ font-size: var(--pl-text-sm);
116
+ color: var(--pl-color-text);
117
+ }
118
+ }
119
+
120
+ @media (forced-colors: active) {
121
+ :where(.c-switch__track) {
122
+ background: ButtonFace;
123
+ border-color: ButtonText;
124
+ }
125
+
126
+ :where(.c-switch:has(.c-switch__input:checked) .c-switch__track) {
127
+ background: Highlight;
128
+ }
129
+
130
+ :where(.c-switch__thumb) {
131
+ background: ButtonText;
132
+ }
133
+
134
+ :where(.c-switch:has(.c-switch__input:checked) .c-switch__thumb) {
135
+ background: HighlightText;
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,164 @@
1
+ /*
2
+ * Porchlight - tabs component
3
+ * ===========================================================================
4
+ * A tabbed region: a [role="tablist"] of [role="tab"] controls that switch
5
+ * between [role="tabpanel"]s. The canonical master-detail / settings /
6
+ * dashboard-sectioning primitive. Wraps real <button> tabs.
7
+ *
8
+ * Behaviour (which tab is active) is APP state, not CSS: the app sets
9
+ * aria-selected on the tab and [hidden] on the inactive panels (exactly as a
10
+ * real app drives <dialog> via showModal). The framework owns the styling and
11
+ * the focus indicator; the roving-tabindex keyboard model is documented as the
12
+ * app's responsibility (arrow/Home/End). This is the same "no JS in the
13
+ * framework" boundary every interactive component observes.
14
+ *
15
+ * The selected indicator is an inset box-shadow on the tab's block-end edge,
16
+ * NOT a border + negative margin: box-shadow never affects layout, so the tab
17
+ * never shifts when it becomes selected, and the indicator cleanly overlaps
18
+ * the tablist's own bottom border.
19
+ *
20
+ * State rules follow the @scope rule of thumb: structural + attribute selectors
21
+ * ([aria-selected], a real attribute) live inside @scope; live-state pseudos
22
+ * (:hover, :focus-visible) live outside it with :where(), because they don't
23
+ * reliably apply to scoped descendants.
24
+ */
25
+ @layer porchlight.components {
26
+ @scope (.c-tabs) {
27
+ :scope {
28
+ --c-tabs-gap: var(--pl-space-4);
29
+ --c-tabs-indicator: 3px;
30
+
31
+ display: grid;
32
+ gap: var(--c-tabs-gap);
33
+ }
34
+
35
+ .c-tabs__list {
36
+ display: flex;
37
+ gap: var(--pl-space-1);
38
+ padding: 0;
39
+ margin: 0;
40
+ list-style: none;
41
+ border-block-end: 1px solid color-mix(
42
+ in oklab,
43
+ var(--pl-color-border),
44
+ transparent 20%
45
+ );
46
+ overflow-x: auto;
47
+ scrollbar-width: none; /* Hide scrollbar in Firefox */
48
+
49
+ &::-webkit-scrollbar {
50
+ display: none; /* Hide scrollbar in Chrome/Safari */
51
+ }
52
+ }
53
+
54
+ .c-tabs__tab {
55
+ --c-tabs-tab-padding: var(--pl-space-3);
56
+
57
+ appearance: none;
58
+ display: inline-flex;
59
+ align-items: center;
60
+ flex-shrink: 0;
61
+ gap: var(--pl-space-2);
62
+ padding: var(--pl-space-2) var(--c-tabs-tab-padding);
63
+ border: 0;
64
+ background: transparent;
65
+ color: var(--pl-color-text-muted);
66
+ font: inherit;
67
+ font-weight: var(--pl-font-weight-medium);
68
+ white-space: nowrap;
69
+ cursor: pointer;
70
+
71
+ /* The selected indicator: a block-end accent line drawn INSIDE the tab.
72
+ Transparent unless selected; box-shadow (not border) so selecting a
73
+ tab never shifts layout. Sits flush over the tablist border. */
74
+ box-shadow: inset 0 calc(var(--c-tabs-indicator) * -1) 0 0 transparent;
75
+ transition:
76
+ color var(--pl-duration-1) var(--pl-ease-standard),
77
+ box-shadow var(--pl-duration-1) var(--pl-ease-standard);
78
+ }
79
+
80
+ /* Selected: accent text, bolder, and the accent indicator line. */
81
+ .c-tabs__tab[aria-selected="true"] {
82
+ color: var(--pl-color-accent);
83
+ font-weight: var(--pl-font-weight-semibold);
84
+ box-shadow: inset 0 calc(var(--c-tabs-indicator) * -1) 0 0
85
+ var(--pl-color-accent);
86
+ }
87
+
88
+ .c-tabs__tab:disabled,
89
+ .c-tabs__tab[aria-disabled="true"] {
90
+ color: var(--pl-color-text-muted);
91
+ opacity: var(--pl-opacity-disabled);
92
+ }
93
+
94
+ /* A trailing count (unread, total) inside a tab - a muted chip. */
95
+ .c-tabs__tab .c-tabs__count {
96
+ padding-inline: var(--pl-space-2);
97
+ border-radius: var(--pl-radius-pill);
98
+ background: var(--pl-color-surface-2);
99
+ color: var(--pl-color-text-muted);
100
+ font-size: var(--pl-text-xs);
101
+ font-weight: var(--pl-font-weight-semibold);
102
+ line-height: var(--pl-leading-snug);
103
+ }
104
+
105
+ .c-tabs__tab[aria-selected="true"] .c-tabs__count {
106
+ background: color-mix(in oklab, var(--pl-color-accent), transparent 82%);
107
+ color: var(--pl-color-accent);
108
+ }
109
+
110
+ /* Panels: block by default; an inactive one is [hidden] in HTML. */
111
+ .c-tabs__panel {
112
+ animation: c-tabs-reveal var(--pl-duration-2) var(--pl-ease-standard);
113
+ }
114
+ }
115
+
116
+ /* Live-state rules OUTSIDE @scope (:hover/:focus-visible on the tab don't
117
+ reliably apply inside @scope). A subtle surface-2 wash on hover aids
118
+ discoverability without competing with the selected indicator. */
119
+ :where(.c-tabs__list)
120
+ .c-tabs__tab:is(:hover, :focus-visible):not(
121
+ :disabled,
122
+ [aria-disabled="true"]
123
+ ) {
124
+ background: var(--pl-color-surface-2);
125
+ color: var(--pl-color-text);
126
+ }
127
+
128
+ /* Hover should NOT fake the accent indicator on an unselected tab - only the
129
+ selected tab carries the line. Restore muted text on hover so colour still
130
+ flips for affordance without implying selection. */
131
+ :where(.c-tabs__list)
132
+ .c-tabs__tab:hover:not(:disabled, [aria-disabled="true"]) {
133
+ color: var(--pl-color-text);
134
+ }
135
+
136
+ :where(.c-tabs__list) .c-tabs__tab:focus-visible {
137
+ outline: var(--pl-focus-size) solid var(--pl-focus-color);
138
+ outline-offset: calc(var(--pl-focus-offset) * -1);
139
+ }
140
+
141
+ /* Reveal animation for a newly-shown panel. Disabled under reduced motion by
142
+ the themes layer's universal 1ms clamp, so it degrades to an instant cut. */
143
+ @keyframes c-tabs-reveal {
144
+ from {
145
+ opacity: 0;
146
+ transform: translateY(2px);
147
+ }
148
+ }
149
+
150
+ @media (forced-colors: active) {
151
+ :where(.c-tabs__list) {
152
+ border-block-end-color: CanvasText;
153
+ }
154
+
155
+ :where(.c-tabs__tab)[aria-selected="true"] {
156
+ color: Highlight;
157
+ box-shadow: inset 0 calc(var(--c-tabs-indicator) * -1) 0 0 Highlight;
158
+ }
159
+
160
+ :where(.c-tabs__tab):focus-visible {
161
+ outline-color: Highlight;
162
+ }
163
+ }
164
+ }
@@ -0,0 +1,77 @@
1
+ /*
2
+ * Porchlight - tag input component
3
+ * ===========================================================================
4
+ * A text input that accepts multiple values as removable chips. The CSS
5
+ * handles the layout (wrapping chips + inline input); JS handles
6
+ * tokenization (enter/comma to add, backspace to remove).
7
+ *
8
+ * Uses field-sizing: content where supported so the input auto-grows.
9
+ * Falls back to a fixed-width input.
10
+ *
11
+ * Structure:
12
+ * <div class="c-tag-input">
13
+ * <span class="c-chip">React <button class="c-chip__remove">x</button></span>
14
+ * <input class="c-tag-input__field" placeholder="Add tag..." />
15
+ * </div>
16
+ */
17
+ @layer porchlight.components {
18
+ @scope (.c-tag-input) {
19
+ :scope {
20
+ --c-tag-input-pad: var(--pl-space-2);
21
+ --c-tag-input-gap: var(--pl-space-2);
22
+ --c-tag-input-min-h: calc(var(--pl-control-block-size) - 2px);
23
+
24
+ display: flex;
25
+ flex-wrap: wrap;
26
+ align-items: center;
27
+ gap: var(--c-tag-input-gap);
28
+ min-block-size: var(--c-tag-input-min-h);
29
+ padding: var(--c-tag-input-pad);
30
+ border: 1px solid var(--pl-color-border);
31
+ border-radius: var(--pl-radius-md);
32
+ background: var(--pl-color-surface);
33
+ cursor: text;
34
+ transition:
35
+ border-color var(--pl-duration-1) var(--pl-ease-standard),
36
+ box-shadow var(--pl-duration-1) var(--pl-ease-standard);
37
+ }
38
+
39
+ :scope:focus-within {
40
+ outline: none;
41
+ border-color: var(--pl-focus-color);
42
+ box-shadow: 0 0 0 4px
43
+ color-mix(in oklab, var(--pl-focus-color), transparent var(--pl-focus-glow-opacity));
44
+ }
45
+
46
+ .c-tag-input__field {
47
+ flex: 1;
48
+ min-inline-size: 6rem;
49
+ padding: 0;
50
+ border: 0;
51
+ background: transparent;
52
+ color: inherit;
53
+ font: inherit;
54
+ font-size: var(--pl-text-sm);
55
+ outline: none;
56
+ }
57
+
58
+ .c-tag-input__field::placeholder {
59
+ color: var(--pl-color-text-muted);
60
+ }
61
+
62
+ /* Auto-grow where supported. */
63
+ @supports (field-sizing: content) {
64
+ .c-tag-input__field {
65
+ field-sizing: content;
66
+ min-inline-size: 4rem;
67
+ }
68
+ }
69
+ }
70
+
71
+ @media (forced-colors: active) {
72
+ :where(.c-tag-input) {
73
+ border-color: ButtonBorder;
74
+ background: Canvas;
75
+ }
76
+ }
77
+ }