@stonedogcode/style 0.12.0 → 0.15.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/README.md +1 -1
- package/package.json +1 -1
- package/src/components/StyledConfetti.tsx +274 -0
- package/src/components/StyledFieldErrors.tsx +94 -0
- package/src/components/StyledForm.tsx +75 -0
- package/src/components/StyledLink.tsx +254 -0
- package/src/components/StyledPage.tsx +274 -0
- package/src/components/StyledTag.tsx +155 -0
- package/src/config/link-component.tsx +69 -0
- package/src/config/style-config.tsx +39 -1
- package/src/index.ts +42 -0
- package/src/preset/index.ts +31 -1
- package/src/preset/recipes/input-bool.ts +111 -10
- package/src/preset/recipes/tag.ts +96 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The props any link implementation must accept.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately the native anchor's own surface plus a required `href`. That is
|
|
9
|
+
* not a compromise to keep the type simple — it is the boundary. A router's
|
|
10
|
+
* link component accepts these and adds its own (prefetch, scroll, replace);
|
|
11
|
+
* this package neither knows nor passes those, so the extra props stay the
|
|
12
|
+
* host's business and no routing concept leaks in here.
|
|
13
|
+
*
|
|
14
|
+
* `href` is a `string`, not `string | UrlObject`. Next.js accepts the object
|
|
15
|
+
* form, but naming it here would put a Next.js type in a package whose whole
|
|
16
|
+
* premise is that it has none — and a host that wants the object form can wrap
|
|
17
|
+
* its own component and take it there, which is exactly what the seam is for.
|
|
18
|
+
*/
|
|
19
|
+
export interface LinkComponentProps
|
|
20
|
+
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
21
|
+
href: string;
|
|
22
|
+
children?: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* What `StyledLink` renders for an in-app destination.
|
|
27
|
+
*
|
|
28
|
+
* A host swaps in its router's link — `next/link`, `react-router`'s `Link`,
|
|
29
|
+
* whatever it has — and gets client-side navigation and prefetching. A host
|
|
30
|
+
* that says nothing gets `DefaultLinkComponent` below.
|
|
31
|
+
*
|
|
32
|
+
* Typed as a component rather than an `ElementType` union so the intrinsic
|
|
33
|
+
* `"a"` string is not a valid value. Allowing it would mean two ways to say the
|
|
34
|
+
* same thing, and the one that looks simpler is the one that cannot be given a
|
|
35
|
+
* `displayName` or wrapped.
|
|
36
|
+
*
|
|
37
|
+
* **It must forward its ref to the underlying anchor.** `RefAttributes` is in
|
|
38
|
+
* the props type rather than left implicit because this package's peer range
|
|
39
|
+
* starts at React 18, where a `ref` handed to a plain function component is not
|
|
40
|
+
* a prop — React 18 drops it and logs "Function components cannot be given
|
|
41
|
+
* refs". Stating it in the type is what makes that a compile error for the host
|
|
42
|
+
* instead of a console warning nobody reads. `next/link` and react-router's
|
|
43
|
+
* `Link` both forward already, so the common cases need nothing.
|
|
44
|
+
*/
|
|
45
|
+
export type LinkComponent = React.ComponentType<
|
|
46
|
+
LinkComponentProps & React.RefAttributes<HTMLAnchorElement>
|
|
47
|
+
>;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A plain anchor, and the reason this seam is safe to leave unconfigured.
|
|
51
|
+
*
|
|
52
|
+
* This is the point of the whole arrangement (NEH-430): the default is not a
|
|
53
|
+
* placeholder that throws, warns, or renders nothing until someone wires a
|
|
54
|
+
* router. It is a **real, correct, accessible link** — it navigates, it opens in
|
|
55
|
+
* a new tab when told to, middle-click and "open in new window" work, and a
|
|
56
|
+
* screen reader announces it as a link. What a host gains by overriding is
|
|
57
|
+
* client-side navigation and prefetching: real benefits, and neither of them
|
|
58
|
+
* load-bearing for the link *working*.
|
|
59
|
+
*
|
|
60
|
+
* A seam whose default is broken is a required configuration step wearing a
|
|
61
|
+
* disguise, and it recreates exactly the adoption deadlock this issue set out
|
|
62
|
+
* to remove.
|
|
63
|
+
*/
|
|
64
|
+
export const DefaultLinkComponent: LinkComponent = React.forwardRef<
|
|
65
|
+
HTMLAnchorElement,
|
|
66
|
+
LinkComponentProps
|
|
67
|
+
>(function DefaultLinkComponent(props, ref) {
|
|
68
|
+
return <a ref={ref} {...props} />;
|
|
69
|
+
}) as LinkComponent;
|
|
@@ -5,6 +5,7 @@ import type { DensityProfile, FontSizeProfile, IconSize, ThemeVariant } from "./
|
|
|
5
5
|
import { resolveDensityStep, type DensityBase, type DensityStep } from "./density";
|
|
6
6
|
import { THEME_VARIANTS } from "./types";
|
|
7
7
|
import { IntentIconProvider, type IntentIcons } from "./intent-icons";
|
|
8
|
+
import { DefaultLinkComponent, type LinkComponent } from "./link-component";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Everything this component library needs to know about the host application.
|
|
@@ -69,6 +70,26 @@ export interface StyleConfig {
|
|
|
69
70
|
* (a business tool for a general audience) sets `"md"`.
|
|
70
71
|
*/
|
|
71
72
|
iconSize: IconSize;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* What `StyledLink` renders for an in-app destination (NEH-430).
|
|
76
|
+
*
|
|
77
|
+
* This is a seam, not a dependency. `StyledLink` used to import `next/link`
|
|
78
|
+
* directly, which is why it could not live in this package at all — CLAUDE.md
|
|
79
|
+
* names `next/*` as forbidden, and three products with three framework
|
|
80
|
+
* choices cannot be made to share one.
|
|
81
|
+
*
|
|
82
|
+
* The default is a plain `<a>`: a real, working, accessible link, not a
|
|
83
|
+
* placeholder. A host passes its router's component to gain client-side
|
|
84
|
+
* navigation and prefetching, and gains nothing else — so an unconfigured
|
|
85
|
+
* host is not broken, merely un-optimised.
|
|
86
|
+
*
|
|
87
|
+
* Note this is the only field here whose value is a *component*. It sits on
|
|
88
|
+
* `StyleConfig` rather than being a `StyledLink` prop because the choice is
|
|
89
|
+
* app-wide by nature: passing it per call site is how ~40 links end up with
|
|
90
|
+
* two navigation behaviours and no way to retune either.
|
|
91
|
+
*/
|
|
92
|
+
linkComponent: LinkComponent;
|
|
72
93
|
}
|
|
73
94
|
|
|
74
95
|
/**
|
|
@@ -91,6 +112,10 @@ export const DEFAULT_STYLE_CONFIG: StyleConfig = {
|
|
|
91
112
|
// what the originating application already renders. Changing it would be an
|
|
92
113
|
// invisible, app-wide visual change to every existing consumer.
|
|
93
114
|
iconSize: "2x",
|
|
115
|
+
// A plain anchor. Unlike `iconSize` above this is genuinely the safe middle:
|
|
116
|
+
// it navigates correctly everywhere, and a host overriding it is opting into
|
|
117
|
+
// an improvement rather than repairing a default.
|
|
118
|
+
linkComponent: DefaultLinkComponent,
|
|
94
119
|
};
|
|
95
120
|
|
|
96
121
|
const StyleConfigContext = createContext<StyleConfig>(DEFAULT_STYLE_CONFIG);
|
|
@@ -143,6 +168,7 @@ export function StonedogStyleProvider({
|
|
|
143
168
|
iconSize,
|
|
144
169
|
density,
|
|
145
170
|
densityBase,
|
|
171
|
+
linkComponent,
|
|
146
172
|
icons,
|
|
147
173
|
}: StonedogStyleProviderProps) {
|
|
148
174
|
const value = useMemo<StyleConfig>(
|
|
@@ -153,8 +179,9 @@ export function StonedogStyleProvider({
|
|
|
153
179
|
iconSize: iconSize ?? DEFAULT_STYLE_CONFIG.iconSize,
|
|
154
180
|
density: density ?? DEFAULT_STYLE_CONFIG.density,
|
|
155
181
|
densityBase: densityBase ?? DEFAULT_STYLE_CONFIG.densityBase,
|
|
182
|
+
linkComponent: linkComponent ?? DEFAULT_STYLE_CONFIG.linkComponent,
|
|
156
183
|
}),
|
|
157
|
-
[fontSizeProfile, variant, iconSize, density, densityBase],
|
|
184
|
+
[fontSizeProfile, variant, iconSize, density, densityBase, linkComponent],
|
|
158
185
|
);
|
|
159
186
|
|
|
160
187
|
return (
|
|
@@ -196,6 +223,17 @@ export function useIconSize(): IconSize {
|
|
|
196
223
|
return useStyleConfig().iconSize;
|
|
197
224
|
}
|
|
198
225
|
|
|
226
|
+
/**
|
|
227
|
+
* The host's link implementation, or a plain `<a>` if it supplied none.
|
|
228
|
+
*
|
|
229
|
+
* Exported so an application component outside this package can render a link
|
|
230
|
+
* the same way `StyledLink` does, without reaching for the router directly and
|
|
231
|
+
* re-creating the coupling the seam removed.
|
|
232
|
+
*/
|
|
233
|
+
export function useLinkComponent(): LinkComponent {
|
|
234
|
+
return useStyleConfig().linkComponent;
|
|
235
|
+
}
|
|
236
|
+
|
|
199
237
|
/**
|
|
200
238
|
* Resolve a control's appearance: **the caller's, else the user's app-wide
|
|
201
239
|
* setting, else `solid`.**
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export {
|
|
|
20
20
|
useStyleConfig,
|
|
21
21
|
useFontSizeProfile,
|
|
22
22
|
useIconSize,
|
|
23
|
+
useLinkComponent,
|
|
23
24
|
useResolvedVariant,
|
|
24
25
|
DEFAULT_STYLE_CONFIG,
|
|
25
26
|
} from "./config/style-config";
|
|
@@ -28,6 +29,10 @@ export type {
|
|
|
28
29
|
StonedogStyleProviderProps,
|
|
29
30
|
} from "./config/style-config";
|
|
30
31
|
|
|
32
|
+
/** The link seam — see `config/link-component.tsx` and NEH-430. */
|
|
33
|
+
export { DefaultLinkComponent } from "./config/link-component";
|
|
34
|
+
export type { LinkComponent, LinkComponentProps } from "./config/link-component";
|
|
35
|
+
|
|
31
36
|
/**
|
|
32
37
|
* Deprecated `Hopper*` aliases — NEH-251. See `config/style-config.tsx`.
|
|
33
38
|
* Removed once every consumer has landed its rename PR.
|
|
@@ -192,6 +197,43 @@ export type { StyledTooltipProps } from "./components/StyledTooltip";
|
|
|
192
197
|
export { default as StyledFormLabel } from "./components/StyledFormLabel";
|
|
193
198
|
export type { StyledFormLabelProps } from "./components/StyledFormLabel";
|
|
194
199
|
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Components that were blocked on a runtime dependency until NEH-430 gave each
|
|
202
|
+
// a seam with a working default. None of them adds a dependency; the host
|
|
203
|
+
// supplies the framework-specific half, or takes the default and loses nothing
|
|
204
|
+
// that stops it working.
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
export { default as StyledLink, StyledLink as Link } from "./components/StyledLink";
|
|
207
|
+
export type { StyledLinkProps, LinkPresentation } from "./components/StyledLink";
|
|
208
|
+
|
|
209
|
+
export { default as StyledTag, StyledTag as Tag } from "./components/StyledTag";
|
|
210
|
+
export type { StyledTagProps, TagTone } from "./components/StyledTag";
|
|
211
|
+
|
|
212
|
+
export {
|
|
213
|
+
default as StyledFieldErrors,
|
|
214
|
+
StyledFieldErrors as FieldErrors,
|
|
215
|
+
} from "./components/StyledFieldErrors";
|
|
216
|
+
export type {
|
|
217
|
+
StyledFieldErrorsProps,
|
|
218
|
+
FieldError,
|
|
219
|
+
} from "./components/StyledFieldErrors";
|
|
220
|
+
|
|
221
|
+
export { default as StyledPage, StyledPage as Page } from "./components/StyledPage";
|
|
222
|
+
export type { StyledPageProps } from "./components/StyledPage";
|
|
223
|
+
|
|
224
|
+
export { default as StyledForm, StyledForm as Form } from "./components/StyledForm";
|
|
225
|
+
export type { StyledFormProps } from "./components/StyledForm";
|
|
226
|
+
|
|
227
|
+
export {
|
|
228
|
+
default as StyledConfetti,
|
|
229
|
+
StyledConfetti as Confetti,
|
|
230
|
+
} from "./components/StyledConfetti";
|
|
231
|
+
export type {
|
|
232
|
+
StyledConfettiProps,
|
|
233
|
+
CelebrateFn,
|
|
234
|
+
CelebrateOptions,
|
|
235
|
+
} from "./components/StyledConfetti";
|
|
236
|
+
|
|
195
237
|
export { default as StyledInputBool } from "./components/StyledInputBool";
|
|
196
238
|
export type { StyledInputBoolProps, InputBoolVariant } from "./components/StyledInputBool";
|
|
197
239
|
export { INPUT_BOOL_VARIANTS } from "./components/StyledInputBool";
|
package/src/preset/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "./recipes/separator";
|
|
26
26
|
import { stackRecipe } from "./recipes/stack";
|
|
27
27
|
import { stripedRecipe } from "./recipes/striped";
|
|
28
|
+
import { tagRecipe } from "./recipes/tag";
|
|
28
29
|
import { textRecipe } from "./recipes/text";
|
|
29
30
|
import { tooltipRecipe } from "./recipes/tooltip";
|
|
30
31
|
|
|
@@ -83,6 +84,7 @@ const recipes = {
|
|
|
83
84
|
separatorVerticalRecipe,
|
|
84
85
|
stackRecipe,
|
|
85
86
|
stripedRecipe,
|
|
87
|
+
tagRecipe,
|
|
86
88
|
textRecipe,
|
|
87
89
|
tooltipRecipe,
|
|
88
90
|
};
|
|
@@ -135,7 +137,7 @@ const staticCssAlignment = [
|
|
|
135
137
|
|
|
136
138
|
/**
|
|
137
139
|
* The @stonedogcode/style Panda preset: colour tokens, breakpoints, keyframes, and
|
|
138
|
-
* the
|
|
140
|
+
* the 23 recipes the component library is built on.
|
|
139
141
|
*
|
|
140
142
|
* Deliberately does NOT set `globalCss`, `preflight`, `include`, or `outdir` —
|
|
141
143
|
* those are application decisions, and a preset that quietly restyles `body` is
|
|
@@ -195,6 +197,34 @@ export function stonedogStylePreset(options: StonedogStylePresetOptions = {}) {
|
|
|
195
197
|
"0%": { transform: "scaleX(0)" },
|
|
196
198
|
"100%": { transform: "scaleX(1)" },
|
|
197
199
|
},
|
|
200
|
+
/**
|
|
201
|
+
* One confetti particle's flight — `StyledConfetti`'s zero-dependency
|
|
202
|
+
* default (NEH-430).
|
|
203
|
+
*
|
|
204
|
+
* The three `--sd-confetti-*` properties are set inline, per
|
|
205
|
+
* particle, so one keyframe serves a whole burst travelling in every
|
|
206
|
+
* direction. A keyframe cannot randomise, and a hundred generated
|
|
207
|
+
* keyframes would be a hundred rules in every consumer's stylesheet.
|
|
208
|
+
*
|
|
209
|
+
* **These are NOT the theme namespace and must not be confused with
|
|
210
|
+
* it.** CLAUDE.md's rule — never write `var(--…)` for anything the
|
|
211
|
+
* HOST supplies — is about `--<prefix>-*` properties that a theme
|
|
212
|
+
* defines and `cssVarPrefix` re-points. These are component-internal,
|
|
213
|
+
* written and read in the same breath by the same component, and
|
|
214
|
+
* named `--sd-confetti-*` precisely so they cannot collide with a
|
|
215
|
+
* host's namespace. The particle's COLOUR still comes from a token.
|
|
216
|
+
*/
|
|
217
|
+
stonedogConfettiBurst: {
|
|
218
|
+
"0%": {
|
|
219
|
+
transform: "translate3d(0, 0, 0) rotate(0deg)",
|
|
220
|
+
opacity: "1",
|
|
221
|
+
},
|
|
222
|
+
"100%": {
|
|
223
|
+
transform:
|
|
224
|
+
"translate3d(var(--sd-confetti-dx, 0), var(--sd-confetti-dy, 0), 0) rotate(var(--sd-confetti-rot, 0deg))",
|
|
225
|
+
opacity: "0",
|
|
226
|
+
},
|
|
227
|
+
},
|
|
198
228
|
},
|
|
199
229
|
recipes,
|
|
200
230
|
},
|
|
@@ -24,9 +24,16 @@ export const inputBoolRecipe = defineSlotRecipe({
|
|
|
24
24
|
* Verified in the component-test harness rather than assumed: a raw
|
|
25
25
|
* checkbox given a red 2px border and a slate background paints as the
|
|
26
26
|
* default white box. The UA draws the widget and discards
|
|
27
|
-
* `background-color
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* `background-color` and `border-*` — while `getComputedStyle`
|
|
28
|
+
* cheerfully reports both, which is what made this recipe look styled
|
|
29
|
+
* for so long.
|
|
30
|
+
*
|
|
31
|
+
* `border-radius` is worse still and worth separating out (NEH-310): it
|
|
32
|
+
* does not even COMPUTE. A control set to `9999px` reports `0px`, so it
|
|
33
|
+
* cannot be asserted on, cannot be differed by, and is not merely
|
|
34
|
+
* invisible. The `borderRadius: "md"` below is therefore inert too; it is
|
|
35
|
+
* kept only because it belongs to the same `appearance: none` fallback
|
|
36
|
+
* set as `border` and `background-color`.
|
|
30
37
|
*
|
|
31
38
|
* The three it DOES honour, and therefore the only levers here:
|
|
32
39
|
* `accent-color` (the checked fill and tick), `box-shadow` (painted
|
|
@@ -77,9 +84,21 @@ export const inputBoolRecipe = defineSlotRecipe({
|
|
|
77
84
|
*
|
|
78
85
|
* `buttonRecipe` expresses outline as a 2px edge with squared corners.
|
|
79
86
|
* A native checkbox discards `border`, so the same reading is carried by
|
|
80
|
-
* a `box-shadow` ring, which it does paint —
|
|
81
|
-
*
|
|
82
|
-
*
|
|
87
|
+
* a `box-shadow` ring, which it does paint — themed. `solid` states
|
|
88
|
+
* `none` explicitly rather than by omission, so switching between them
|
|
89
|
+
* cannot leave a ring behind.
|
|
90
|
+
*
|
|
91
|
+
* **The squared corners were dropped in NEH-310, because they never
|
|
92
|
+
* existed.** This comment used to say the ring was "squared to match",
|
|
93
|
+
* and the variant carried `borderRadius: "0"` to do it. Probed in the
|
|
94
|
+
* harness: Chromium computes `border-radius: 0px` on a checkbox at
|
|
95
|
+
* `appearance: auto` **whatever the stylesheet says** — a control set to
|
|
96
|
+
* `9999px` reports `0px`, while a plain `<span>` beside it reports its
|
|
97
|
+
* `12px` correctly. So the property is not merely discarded at paint
|
|
98
|
+
* time like `background-color`; it does not even compute, and no variant
|
|
99
|
+
* here can differ by corner. The declaration is removed rather than left
|
|
100
|
+
* as decoration, since a recipe full of inert declarations is the exact
|
|
101
|
+
* condition that made this defect take three issues to find.
|
|
83
102
|
*/
|
|
84
103
|
solid: {
|
|
85
104
|
control: {
|
|
@@ -100,18 +119,60 @@ export const inputBoolRecipe = defineSlotRecipe({
|
|
|
100
119
|
* different — and made a ticked outline checkbox a dark box on a dark
|
|
101
120
|
* surface, which is the checked state, the one thing the control
|
|
102
121
|
* exists to communicate. Distinguishing an appearance must not cost
|
|
103
|
-
* state legibility, so the difference is carried entirely by the
|
|
104
|
-
*
|
|
122
|
+
* state legibility, so the difference is carried entirely by the
|
|
123
|
+
* ring.
|
|
105
124
|
*/
|
|
106
125
|
accentColor: "buttonBgPrimary",
|
|
107
126
|
boxShadow: "0 0 0 2px {colors.borderBgPrimary}",
|
|
108
|
-
borderRadius: "0",
|
|
109
127
|
},
|
|
110
128
|
},
|
|
129
|
+
/**
|
|
130
|
+
* The remaining variants, given a painted difference (NEH-310).
|
|
131
|
+
*
|
|
132
|
+
* NEH-234 fixed `solid` vs `outline` and stopped there, so these still
|
|
133
|
+
* differed only in `background`, `background-image`, `color` and a
|
|
134
|
+
* pseudo-element — every one of which this control discards. A user
|
|
135
|
+
* picking `aurora` app-wide watched every other control change and every
|
|
136
|
+
* checkbox stay put: the same complaint NEH-234 was filed for, one layer
|
|
137
|
+
* down.
|
|
138
|
+
*
|
|
139
|
+
* ## The rule they all follow
|
|
140
|
+
*
|
|
141
|
+
* **Appearance is carried by the ring; the checked colour never varies.**
|
|
142
|
+
*
|
|
143
|
+
* Not a stylistic choice — it is the lesson recorded on `outline` above.
|
|
144
|
+
* Giving a variant a recessive `accentColor` did make it more distinct,
|
|
145
|
+
* and made a ticked box dark-on-dark: illegible in the one state the
|
|
146
|
+
* control exists to communicate. So every variant keeps
|
|
147
|
+
* `accentColor: buttonBgPrimary` and differs by `box-shadow` alone.
|
|
148
|
+
*
|
|
149
|
+
* `outline` (the CSS property) is not available as a lever either: it is
|
|
150
|
+
* the focus ring, and a variant using it would look permanently focused.
|
|
151
|
+
* `border-radius` is not available because it does not even COMPUTE here
|
|
152
|
+
* — see the note on the `outline` variant.
|
|
153
|
+
*
|
|
154
|
+
* ## What this deliberately does NOT attempt
|
|
155
|
+
*
|
|
156
|
+
* `aurora` is a gradient and `glass` is a blur; a box-shadow ring is
|
|
157
|
+
* neither. These are **approximations** — a two-tone ring, a soft halo —
|
|
158
|
+
* not renderings of the intent. The real thing needs `appearance: none`
|
|
159
|
+
* plus a hand-drawn tick, which means owning forced-colors mode and every
|
|
160
|
+
* engine's default widget, and this repo's CT tier is Chromium-only so it
|
|
161
|
+
* cannot answer that. NEH-310 names it as option 2 and says it needs
|
|
162
|
+
* someone to look at the result in more than one engine.
|
|
163
|
+
*
|
|
164
|
+
* The unpainted `bg` / `color` / gradient declarations are left exactly
|
|
165
|
+
* as they were, per the note in `base`.
|
|
166
|
+
*/
|
|
111
167
|
aurora: {
|
|
112
168
|
control: {
|
|
113
169
|
backgroundImage: "linear-gradient(to right, #ff7e5f, #feb47b)",
|
|
114
170
|
color: "buttonTextPrimary",
|
|
171
|
+
accentColor: "buttonBgPrimary",
|
|
172
|
+
// Two stops, two rings — the nearest a box-shadow gets to the
|
|
173
|
+
// gradient this variant means. Layers paint inner-first.
|
|
174
|
+
boxShadow:
|
|
175
|
+
"0 0 0 2px {colors.borderBgAccent}, 0 0 0 4px {colors.borderBgPrimary}",
|
|
115
176
|
},
|
|
116
177
|
},
|
|
117
178
|
glass: {
|
|
@@ -120,7 +181,6 @@ export const inputBoolRecipe = defineSlotRecipe({
|
|
|
120
181
|
overflow: "hidden",
|
|
121
182
|
bg: "buttonBgPrimary/20",
|
|
122
183
|
color: "textPrimary/10",
|
|
123
|
-
boxShadow: "xl",
|
|
124
184
|
backdropFilter: "blur(8px)",
|
|
125
185
|
fontWeight: "bold",
|
|
126
186
|
lineHeight: "shorter",
|
|
@@ -135,6 +195,19 @@ export const inputBoolRecipe = defineSlotRecipe({
|
|
|
135
195
|
"linear(to-br, rgba(255,255,255,0.1), rgba(255,255,255,0.05))",
|
|
136
196
|
zIndex: -1,
|
|
137
197
|
},
|
|
198
|
+
accentColor: "buttonBgPrimary",
|
|
199
|
+
// A soft halo rather than a hard edge — the nearest painted reading
|
|
200
|
+
// of "frosted".
|
|
201
|
+
//
|
|
202
|
+
// This REPLACES the `boxShadow: "xl"` this variant used to carry
|
|
203
|
+
// (removed above, not shadowed — a second `boxShadow` key here was a
|
|
204
|
+
// TS1117 duplicate-property error that the CSS build silently
|
|
205
|
+
// resolved in favour of the last one). `xl` was the only thing glass
|
|
206
|
+
// ever painted, and it is Panda's own neutral shadow: the same grey
|
|
207
|
+
// in every theme this package can wear, which is the one thing a
|
|
208
|
+
// themeable package must not ship.
|
|
209
|
+
boxShadow:
|
|
210
|
+
"0 0 0 1px {colors.borderBgSecondary}, 0 0 12px 2px {colors.boxshadowBgAccent}",
|
|
138
211
|
},
|
|
139
212
|
},
|
|
140
213
|
matte: {
|
|
@@ -145,18 +218,46 @@ export const inputBoolRecipe = defineSlotRecipe({
|
|
|
145
218
|
// is a FIXED dark gradient, so themed text on it risks dark-on-dark.
|
|
146
219
|
color: "white",
|
|
147
220
|
fontWeight: "bold",
|
|
221
|
+
accentColor: "buttonBgPrimary",
|
|
222
|
+
// Wide, blurred and low-contrast: a matte surface absorbs light
|
|
223
|
+
// rather than edging it. The only soft-edged ring in the set, so it
|
|
224
|
+
// cannot be mistaken for `outline` at a glance.
|
|
225
|
+
boxShadow: "0 2px 8px 0 {colors.boxshadowBgSecondary}",
|
|
148
226
|
},
|
|
149
227
|
},
|
|
150
228
|
ghost: {
|
|
151
229
|
control: {
|
|
152
230
|
color: "textSecondary",
|
|
153
231
|
bg: "buttonBgSecondary",
|
|
232
|
+
accentColor: "buttonBgPrimary",
|
|
233
|
+
// The thinnest ring in the set, in the secondary border colour.
|
|
234
|
+
// `ghost` means "present but not asserting itself", which every other
|
|
235
|
+
// recipe expresses by having no fill — exactly the property this
|
|
236
|
+
// control discards.
|
|
237
|
+
boxShadow: "0 0 0 1px {colors.borderBgSecondary}",
|
|
154
238
|
},
|
|
155
239
|
},
|
|
240
|
+
/**
|
|
241
|
+
* `none` is the one variant that CANNOT be distinguished, and saying so
|
|
242
|
+
* is more useful than inventing a difference (NEH-310).
|
|
243
|
+
*
|
|
244
|
+
* Every lever this control has is additive — a ring, a halo, a checked
|
|
245
|
+
* colour. `none` means "do not style this", so the only honest rendering
|
|
246
|
+
* of it is the bare widget, which is what `solid` already is. Giving it a
|
|
247
|
+
* ring to make a test pass would mean the variant named `none` was the
|
|
248
|
+
* only one wearing decoration.
|
|
249
|
+
*
|
|
250
|
+
* So `none` and `solid` render identically, deliberately, and the
|
|
251
|
+
* component test asserts that pair is equal rather than skipping it —
|
|
252
|
+
* so if a future `appearance: none` redesign (option 2 on NEH-310) makes
|
|
253
|
+
* them separable, the test says so instead of quietly passing.
|
|
254
|
+
*/
|
|
156
255
|
none: {
|
|
157
256
|
control: {
|
|
158
257
|
color: "buttonTextPrimary",
|
|
159
258
|
bg: "gray.300",
|
|
259
|
+
accentColor: "buttonBgPrimary",
|
|
260
|
+
boxShadow: "none",
|
|
160
261
|
},
|
|
161
262
|
},
|
|
162
263
|
button: {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { defineRecipe } from "@pandacss/dev";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A small label, in one of six tones.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this is a recipe and not `css()` in the component
|
|
7
|
+
*
|
|
8
|
+
* `StyledTag` painted itself with an inline `css()` call, which was right while
|
|
9
|
+
* it had exactly one appearance. A tone chosen at runtime is a different
|
|
10
|
+
* problem: Panda's extractor reads source text, so `tone={STATUS_COLOR[status]}`
|
|
11
|
+
* resolves to nothing and emits no rule — while the class name still lands in
|
|
12
|
+
* the DOM. The result is an unstyled tag, with no build error and no console
|
|
13
|
+
* warning.
|
|
14
|
+
*
|
|
15
|
+
* Recipes are the escape from that, because `staticCssRecipes` in
|
|
16
|
+
* `preset/index.ts` forces **every variant of every recipe** into the
|
|
17
|
+
* stylesheet. So a tone computed from a status map is covered for free, which is
|
|
18
|
+
* how the consuming apps actually use this (NEH-721: HopperGuard drives 109 of
|
|
19
|
+
* its 155 tags from `STATUS_COLOR[item.status]` and friends).
|
|
20
|
+
*
|
|
21
|
+
* ## The tones reuse StyledAlert's vocabulary exactly
|
|
22
|
+
*
|
|
23
|
+
* `info` / `success` / `warning` / `error` are `AlertStatus`, and each pairs the
|
|
24
|
+
* **same tokens** the alert recipe pairs. Two status vocabularies in one package
|
|
25
|
+
* — one for banners and a different one for tags — is how a product ends up with
|
|
26
|
+
* a green that means "success" in one place and "active" in another.
|
|
27
|
+
*
|
|
28
|
+
* Two tones are additional rather than borrowed:
|
|
29
|
+
*
|
|
30
|
+
* - **`neutral`** is the historical default and stays the default, so every
|
|
31
|
+
* existing call site renders exactly as it did before this variant existed.
|
|
32
|
+
* - **`accent`** has no alert equivalent, because an alert is always *about*
|
|
33
|
+
* something being fine or not. A tag is often just a category — a label, a
|
|
34
|
+
* type, a group — and forcing those into `info` would make "informational"
|
|
35
|
+
* mean nothing.
|
|
36
|
+
*
|
|
37
|
+
* ## No border, unlike the alert
|
|
38
|
+
*
|
|
39
|
+
* The alert recipe pairs each background with a `border*` token. A tag is small
|
|
40
|
+
* and usually appears in groups; a 1px edge on each turns a row of five into
|
|
41
|
+
* visual noise, and the tinted background already separates it from the page.
|
|
42
|
+
* The border tokens stay available if a consumer disagrees.
|
|
43
|
+
*/
|
|
44
|
+
export const tagRecipe = defineRecipe({
|
|
45
|
+
className: "tag",
|
|
46
|
+
base: {
|
|
47
|
+
display: "inline-flex",
|
|
48
|
+
alignItems: "center",
|
|
49
|
+
gap: "1",
|
|
50
|
+
paddingInline: "2",
|
|
51
|
+
/*
|
|
52
|
+
* Vertical padding is deliberately absent: the height comes from the line
|
|
53
|
+
* box and the horizontal padding, so a tag tracks the font scale instead of
|
|
54
|
+
* needing a re-tune whenever it moves.
|
|
55
|
+
*/
|
|
56
|
+
borderRadius: "md",
|
|
57
|
+
/*
|
|
58
|
+
* Not a tap target. A plain tag is not interactive, so the 48px floor does
|
|
59
|
+
* not apply to it — the remove BUTTON inside `StyledTag` is, and states its
|
|
60
|
+
* own.
|
|
61
|
+
*/
|
|
62
|
+
fontSize: "sm",
|
|
63
|
+
whiteSpace: "nowrap",
|
|
64
|
+
},
|
|
65
|
+
variants: {
|
|
66
|
+
tone: {
|
|
67
|
+
neutral: {
|
|
68
|
+
backgroundColor: "boxBgSecondary",
|
|
69
|
+
color: "textSecondary",
|
|
70
|
+
},
|
|
71
|
+
info: {
|
|
72
|
+
backgroundColor: "boxInfo",
|
|
73
|
+
color: "textMain",
|
|
74
|
+
},
|
|
75
|
+
success: {
|
|
76
|
+
backgroundColor: "boxSuccess",
|
|
77
|
+
color: "textSuccess",
|
|
78
|
+
},
|
|
79
|
+
warning: {
|
|
80
|
+
backgroundColor: "boxWarning",
|
|
81
|
+
color: "textWarning",
|
|
82
|
+
},
|
|
83
|
+
error: {
|
|
84
|
+
backgroundColor: "boxError",
|
|
85
|
+
color: "textError",
|
|
86
|
+
},
|
|
87
|
+
accent: {
|
|
88
|
+
backgroundColor: "boxBgAccent",
|
|
89
|
+
color: "textMain",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
defaultVariants: {
|
|
94
|
+
tone: "neutral",
|
|
95
|
+
},
|
|
96
|
+
});
|