@stonedogcode/style 0.9.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/LICENSE +201 -0
- package/NOTICE +18 -0
- package/README.md +699 -0
- package/package.json +95 -0
- package/src/components/DictationControls.tsx +141 -0
- package/src/components/DictationPrompt.tsx +78 -0
- package/src/components/StyledBox.tsx +174 -0
- package/src/components/StyledButton.tsx +144 -0
- package/src/components/StyledCollapsible.tsx +127 -0
- package/src/components/StyledDefinitionList.tsx +134 -0
- package/src/components/StyledFieldset.tsx +157 -0
- package/src/components/StyledFlex.tsx +13 -0
- package/src/components/StyledFooter.tsx +399 -0
- package/src/components/StyledFormLabel.tsx +141 -0
- package/src/components/StyledGrid.tsx +109 -0
- package/src/components/StyledGridItem.tsx +19 -0
- package/src/components/StyledHStack.tsx +145 -0
- package/src/components/StyledHeading.tsx +79 -0
- package/src/components/StyledHrRule.tsx +33 -0
- package/src/components/StyledIcon.tsx +172 -0
- package/src/components/StyledIconButton.tsx +135 -0
- package/src/components/StyledInputBool.tsx +81 -0
- package/src/components/StyledInputRadio.tsx +141 -0
- package/src/components/StyledInputSelect.tsx +115 -0
- package/src/components/StyledInputSlider.tsx +83 -0
- package/src/components/StyledInputText.tsx +146 -0
- package/src/components/StyledInputTextArea.tsx +119 -0
- package/src/components/StyledInputToggle.tsx +224 -0
- package/src/components/StyledList.tsx +188 -0
- package/src/components/StyledScrollbar.tsx +53 -0
- package/src/components/StyledSearch.tsx +78 -0
- package/src/components/StyledSeparator.tsx +38 -0
- package/src/components/StyledSidebar.tsx +555 -0
- package/src/components/StyledSimpleGrid.tsx +99 -0
- package/src/components/StyledSparkLine.tsx +119 -0
- package/src/components/StyledSpinner.tsx +91 -0
- package/src/components/StyledStack.tsx +62 -0
- package/src/components/StyledText.tsx +99 -0
- package/src/components/StyledTooltip.tsx +398 -0
- package/src/components/StyledVStack.tsx +143 -0
- package/src/components/TitleLogo.tsx +223 -0
- package/src/components/create-icon.tsx +66 -0
- package/src/components/create-intent-button.tsx +134 -0
- package/src/components/dictation.ts +71 -0
- package/src/components/intent-buttons.ts +154 -0
- package/src/config/can-hover.ts +75 -0
- package/src/config/density.ts +138 -0
- package/src/config/font-size.ts +113 -0
- package/src/config/intent-icons.tsx +116 -0
- package/src/config/logger.ts +60 -0
- package/src/config/style-config.tsx +263 -0
- package/src/config/types.ts +137 -0
- package/src/index.ts +259 -0
- package/src/preset/index.ts +243 -0
- package/src/preset/recipes/arrows.ts +29 -0
- package/src/preset/recipes/box.ts +122 -0
- package/src/preset/recipes/button.ts +161 -0
- package/src/preset/recipes/dl-list.ts +109 -0
- package/src/preset/recipes/drawer.ts +125 -0
- package/src/preset/recipes/form.ts +95 -0
- package/src/preset/recipes/icon-button.ts +161 -0
- package/src/preset/recipes/icon.ts +34 -0
- package/src/preset/recipes/input-bool.ts +184 -0
- package/src/preset/recipes/input-dropdown.ts +93 -0
- package/src/preset/recipes/input-radio.ts +158 -0
- package/src/preset/recipes/input-surface.ts +152 -0
- package/src/preset/recipes/input-text.ts +17 -0
- package/src/preset/recipes/list.ts +196 -0
- package/src/preset/recipes/menu.ts +28 -0
- package/src/preset/recipes/separator.ts +89 -0
- package/src/preset/recipes/stack.ts +89 -0
- package/src/preset/recipes/striped.ts +34 -0
- package/src/preset/recipes/text.ts +41 -0
- package/src/preset/recipes/tooltip.ts +77 -0
- package/src/preset/semantic-variables.ts +283 -0
package/package.json
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stonedogcode/style",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "A Panda CSS design system: a themeable Panda preset plus the React components built on it.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "StoneDogCode L.L.C.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/stonedog-code/stonedog-style.git"
|
|
10
|
+
},
|
|
11
|
+
"//publishConfig": "A SCOPED package defaults to access: restricted. Publishing one without this succeeds, prints nothing unusual, and then 404s for every consumer — which reads as a missing package rather than as a private one. It was not needed while the name was unscoped (those default to public), so it is new as of NEH-482 and it is the single thing most likely to be forgotten in a scope migration.",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"//exports": "This package ships TypeScript SOURCE, not a bundled dist/. Panda CSS extracts styles by statically parsing source files, so a consumer's `panda codegen` must be able to read our .tsx. A pre-bundled dist would emit class names Panda never generated a stylesheet for. Consumers transpile us (Next.js: `transpilePackages: ['@stonedogcode/style']`) and add our src to their panda `include` globs. See CLAUDE.md.",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./src/index.ts",
|
|
20
|
+
"./preset": "./src/preset/index.ts",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"//files": "The package ships SOURCE, so `src` has to go out whole — but only the part a consumer imports. Tests, Playwright component tests and their harnesses were 57 of 130 entries and never resolvable from any export, so every one of them was a file a consumer's Panda run had to be told to skip. `src/__tests__/published-package.test.ts` asserts this listing rather than this array: npm's own resolution of these negations is the only thing that decides what ships.",
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"!src/**/__tests__/**",
|
|
27
|
+
"!src/**/*.ct.tsx",
|
|
28
|
+
"!src/**/*.harness.tsx",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"NOTICE",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"panda:prepare": "panda codegen",
|
|
35
|
+
"panda:build": "panda codegen && panda cssgen --outfile styled-system/styles.css",
|
|
36
|
+
"panda:watch": "panda --watch",
|
|
37
|
+
"prepare": "npm run panda:prepare",
|
|
38
|
+
"pretype-check": "npm run panda:prepare",
|
|
39
|
+
"type-check": "tsc --noEmit",
|
|
40
|
+
"lint": "eslint . --ext ts,tsx",
|
|
41
|
+
"//pretest": "panda:build, not panda:prepare — the token-contract suite asserts against the generated stylesheet, which cssgen produces and codegen alone does not.",
|
|
42
|
+
"pretest": "npm run panda:build",
|
|
43
|
+
"test": "jest",
|
|
44
|
+
"pretest:ct": "npm run panda:build",
|
|
45
|
+
"test:ct": "playwright test --config playwright-ct.config.ts",
|
|
46
|
+
"test:ct:ui": "playwright test --config playwright-ct.config.ts --ui",
|
|
47
|
+
"test:coverage": "jest --coverage",
|
|
48
|
+
"gate": "npm run type-check && npm run lint && npm run test",
|
|
49
|
+
"//publish:stonedog-style": "Publish to npm, end to end. Run from a terminal, interactively — npm prompts for the 2FA one-time password and the login flow needs a browser. It refuses a checkout that is detached, dirty, or behind origin/main: a submodule sits detached at the consumer's gitlink by default, and publishing from one commit behind ships a tarball missing the very thing you are publishing for while looking like a success (it did, on 2026-08-04, without TitleLogo.tsx). Runs the gate, prints the tarball listing, then proves the result by installing from the registry into a temp directory.",
|
|
50
|
+
"publish:stonedog-style": "bash scripts/publish-package.sh",
|
|
51
|
+
"version:bump:patch": "npm version patch --no-git-tag-version",
|
|
52
|
+
"version:bump:minor": "npm version minor --no-git-tag-version",
|
|
53
|
+
"audit": "npm audit --audit-level=moderate"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@pandacss/dev": ">=1.9.0",
|
|
57
|
+
"react": ">=18",
|
|
58
|
+
"react-dom": ">=18"
|
|
59
|
+
},
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"csstype": "^3.2.3"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@pandacss/dev": "^1.11.1",
|
|
65
|
+
"@playwright/experimental-ct-react": "^1.62.1",
|
|
66
|
+
"@playwright/test": "^1.62.1",
|
|
67
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
68
|
+
"@testing-library/react": "^16.3.2",
|
|
69
|
+
"@testing-library/user-event": "^14.6.1",
|
|
70
|
+
"@types/jest": "^29.5.14",
|
|
71
|
+
"@types/react": "^19.2.15",
|
|
72
|
+
"@types/react-dom": "^19.2.3",
|
|
73
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
74
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
75
|
+
"eslint": "^8.57.1",
|
|
76
|
+
"eslint-plugin-react": "^7.37.0",
|
|
77
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
78
|
+
"jest": "^29.7.0",
|
|
79
|
+
"jest-environment-jsdom": "^29.7.0",
|
|
80
|
+
"react": "^19.0.4",
|
|
81
|
+
"react-dom": "^19.0.4",
|
|
82
|
+
"ts-jest": "^29.4.11",
|
|
83
|
+
"typescript": "^5.9.3"
|
|
84
|
+
},
|
|
85
|
+
"engines": {
|
|
86
|
+
"node": ">=20"
|
|
87
|
+
},
|
|
88
|
+
"keywords": [
|
|
89
|
+
"panda-css",
|
|
90
|
+
"design-system",
|
|
91
|
+
"react",
|
|
92
|
+
"components",
|
|
93
|
+
"theme"
|
|
94
|
+
]
|
|
95
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { styled } from "styled-system/jsx";
|
|
5
|
+
import { useIntentIcon } from "../config/intent-icons";
|
|
6
|
+
import type { Dictation } from "./dictation";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The mic (and optional redo) buttons that sit inside a dictatable field.
|
|
10
|
+
*
|
|
11
|
+
* Shared by `StyledInputText` and `StyledInputTextArea` so the two cannot
|
|
12
|
+
* drift — they had already drifted in the app this came from, where only one
|
|
13
|
+
* of them offered redo.
|
|
14
|
+
*
|
|
15
|
+
* ## Accessibility, which is most of what this component is
|
|
16
|
+
*
|
|
17
|
+
* - **`aria-pressed`, not a label that changes.** A toggle whose name flips
|
|
18
|
+
* between "Dictate" and "Stop" reads as a different control each time and
|
|
19
|
+
* breaks voice input ("click Dictate" stops matching once recording starts).
|
|
20
|
+
* The name stays put; the state is what changes.
|
|
21
|
+
* - **Recording is not signalled by colour alone.** Colour drives the icon,
|
|
22
|
+
* `aria-pressed` drives assistive tech, and the two together mean the state
|
|
23
|
+
* survives both a screen reader and achromatopsia.
|
|
24
|
+
* - **44×44 minimum.** These sit inside a field and the temptation is to make
|
|
25
|
+
* them small enough not to crowd the text. The floor wins; the field gets
|
|
26
|
+
* padding instead.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export interface DictationControlsProps {
|
|
30
|
+
dictation: Dictation;
|
|
31
|
+
/** Accessible name for the mic. */
|
|
32
|
+
micLabel: string;
|
|
33
|
+
/** Accessible name for redo. */
|
|
34
|
+
redoLabel: string;
|
|
35
|
+
/**
|
|
36
|
+
* Where the buttons sit vertically.
|
|
37
|
+
*
|
|
38
|
+
* `center` for a single-line field. `top` for a textarea, which grows — a
|
|
39
|
+
* centred button would drift down the field as the user typed and end up
|
|
40
|
+
* beside the middle of their text.
|
|
41
|
+
*/
|
|
42
|
+
anchor?: "center" | "top";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Room the field must leave on its trailing edge so text clears the buttons. */
|
|
46
|
+
export function dictationPadding(dictation: Dictation | undefined): string | undefined {
|
|
47
|
+
if (!dictation?.isSupported) return undefined;
|
|
48
|
+
return dictation.showRedo ? "5.5em" : "3.5em";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ControlButton = styled("button", {
|
|
52
|
+
base: {
|
|
53
|
+
display: "inline-flex",
|
|
54
|
+
alignItems: "center",
|
|
55
|
+
justifyContent: "center",
|
|
56
|
+
// WCAG 2.5.5. Non-negotiable even though these live inside a field —
|
|
57
|
+
// the field pads itself to make room rather than shrinking them.
|
|
58
|
+
minWidth: "48px",
|
|
59
|
+
minHeight: "48px",
|
|
60
|
+
background: "transparent",
|
|
61
|
+
border: "none",
|
|
62
|
+
padding: "0.25em",
|
|
63
|
+
cursor: "pointer",
|
|
64
|
+
lineHeight: "0",
|
|
65
|
+
borderRadius: "md",
|
|
66
|
+
color: "textPrimary",
|
|
67
|
+
_hover: { color: "textAccent" },
|
|
68
|
+
},
|
|
69
|
+
variants: {
|
|
70
|
+
recording: {
|
|
71
|
+
// A token, not `var(--colors-red-500, #e53e3e)` as the originating code
|
|
72
|
+
// had — that namespace is undefined under every prefix, so the literal
|
|
73
|
+
// always won and the recording state ignored the theme entirely.
|
|
74
|
+
true: { color: "textError" },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export default function DictationControls({
|
|
80
|
+
dictation,
|
|
81
|
+
micLabel,
|
|
82
|
+
redoLabel,
|
|
83
|
+
anchor = "center",
|
|
84
|
+
}: DictationControlsProps) {
|
|
85
|
+
const micIcon = useIntentIcon("dictate");
|
|
86
|
+
const redoIcon = useIntentIcon("redo");
|
|
87
|
+
|
|
88
|
+
if (!dictation.isSupported) return null;
|
|
89
|
+
|
|
90
|
+
const vertical: React.CSSProperties =
|
|
91
|
+
anchor === "top"
|
|
92
|
+
? { top: "0.25em" }
|
|
93
|
+
: { top: "50%", transform: "translateY(-50%)" };
|
|
94
|
+
|
|
95
|
+
// ONE absolutely-positioned row, not two independently-offset buttons.
|
|
96
|
+
//
|
|
97
|
+
// The first version gave each button its own `right` offset — `3em` and
|
|
98
|
+
// `0.25em` — and they overlapped by 7px at every viewport. `em` resolves
|
|
99
|
+
// against inherited font size, so the gap between two 44px buttons was
|
|
100
|
+
// expressed in a unit with no relationship to 44px, and any change to the
|
|
101
|
+
// surrounding type scale would have moved them again. Flex owns the gap now
|
|
102
|
+
// and the arithmetic is gone.
|
|
103
|
+
//
|
|
104
|
+
// Redo comes first in DOM order, so it sits inboard and the mic stays on the
|
|
105
|
+
// edge where the user learned it — and tab order matches reading order.
|
|
106
|
+
return (
|
|
107
|
+
<div
|
|
108
|
+
style={{
|
|
109
|
+
position: "absolute",
|
|
110
|
+
right: "0.25em",
|
|
111
|
+
display: "flex",
|
|
112
|
+
alignItems: "center",
|
|
113
|
+
gap: "0.25em",
|
|
114
|
+
...vertical,
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{/* Suppressed while the continue prompt is up — see `Dictation`. */}
|
|
118
|
+
{dictation.showRedo && dictation.redo && !dictation.continuePrompt && (
|
|
119
|
+
<ControlButton
|
|
120
|
+
type="button"
|
|
121
|
+
aria-label={redoLabel}
|
|
122
|
+
data-testid="dictation-redo"
|
|
123
|
+
onClick={dictation.redo}
|
|
124
|
+
>
|
|
125
|
+
{redoIcon}
|
|
126
|
+
</ControlButton>
|
|
127
|
+
)}
|
|
128
|
+
<ControlButton
|
|
129
|
+
type="button"
|
|
130
|
+
aria-label={micLabel}
|
|
131
|
+
// The name is constant and the STATE changes — see the note above.
|
|
132
|
+
aria-pressed={dictation.isListening}
|
|
133
|
+
recording={dictation.isListening}
|
|
134
|
+
data-testid="dictation-mic"
|
|
135
|
+
onClick={dictation.onMicClick}
|
|
136
|
+
>
|
|
137
|
+
{micIcon}
|
|
138
|
+
</ControlButton>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { styled } from "styled-system/jsx";
|
|
5
|
+
import StyledButton from "./StyledButton";
|
|
6
|
+
import StyledText from "./StyledText";
|
|
7
|
+
import type { Dictation } from "./dictation";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* "Add to what you already wrote?" — the choice a second recording forces.
|
|
11
|
+
*
|
|
12
|
+
* Renders **below** the field, not inside it, and that is the whole design.
|
|
13
|
+
* The mic and redo buttons are affordances: small, in the corner, ignorable.
|
|
14
|
+
* This is a question with two answers and a wrong one that destroys work, so it
|
|
15
|
+
* takes up room, sits in the reading path, and does not go away on its own.
|
|
16
|
+
*
|
|
17
|
+
* It exists because the alternative is guessing. A user who dictates a
|
|
18
|
+
* paragraph, stops, thinks, and presses the mic again has not told anyone
|
|
19
|
+
* whether they meant to add or replace — and silently replacing is a data-loss
|
|
20
|
+
* bug wearing the costume of a UX preference.
|
|
21
|
+
*
|
|
22
|
+
* Announced as a `group` with an accessible name rather than a live region: it
|
|
23
|
+
* appears in response to the user's own action, so they are already attending
|
|
24
|
+
* to it, and an alert would interrupt a screen reader mid-sentence for
|
|
25
|
+
* something they just asked for.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export interface DictationPromptProps {
|
|
29
|
+
dictation: Dictation;
|
|
30
|
+
question: string;
|
|
31
|
+
continueLabel: string;
|
|
32
|
+
startOverLabel: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const PromptRow = styled("div", {
|
|
36
|
+
base: {
|
|
37
|
+
display: "flex",
|
|
38
|
+
flexWrap: "wrap",
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
gap: "2",
|
|
41
|
+
marginTop: "2",
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export default function DictationPrompt({
|
|
46
|
+
dictation,
|
|
47
|
+
question,
|
|
48
|
+
continueLabel,
|
|
49
|
+
startOverLabel,
|
|
50
|
+
}: DictationPromptProps) {
|
|
51
|
+
if (!dictation.continuePrompt) return null;
|
|
52
|
+
if (!dictation.chooseContinue || !dictation.chooseStartOver) return null;
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<PromptRow role="group" aria-label={question} data-testid="dictation-continue-prompt">
|
|
56
|
+
<StyledText>{question}</StyledText>
|
|
57
|
+
{/* Real buttons, not bare styled elements — they inherit the 44px floor
|
|
58
|
+
and the theme like every other control. Continue comes first because
|
|
59
|
+
it is the non-destructive answer. */}
|
|
60
|
+
<StyledButton
|
|
61
|
+
type="button"
|
|
62
|
+
variant="outline"
|
|
63
|
+
data-testid="dictation-continue"
|
|
64
|
+
onClick={dictation.chooseContinue}
|
|
65
|
+
>
|
|
66
|
+
{continueLabel}
|
|
67
|
+
</StyledButton>
|
|
68
|
+
<StyledButton
|
|
69
|
+
type="button"
|
|
70
|
+
variant="ghost"
|
|
71
|
+
data-testid="dictation-start-over"
|
|
72
|
+
onClick={dictation.chooseStartOver}
|
|
73
|
+
>
|
|
74
|
+
{startOverLabel}
|
|
75
|
+
</StyledButton>
|
|
76
|
+
</PromptRow>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { styled } from "styled-system/jsx";
|
|
3
|
+
import { boxRecipe, type BoxRecipeVariantProps } from "styled-system/recipes";
|
|
4
|
+
import type { HTMLStyledProps, ConditionalValue } from "styled-system/types";
|
|
5
|
+
import { Property } from "csstype";
|
|
6
|
+
import { css, cx } from "styled-system/css";
|
|
7
|
+
import StyledGrid from "./StyledGrid";
|
|
8
|
+
import StyledVStack from "./StyledVStack";
|
|
9
|
+
|
|
10
|
+
interface StyledGridPanelProps {
|
|
11
|
+
leftPanel?: React.ReactNode;
|
|
12
|
+
rightPanel?: React.ReactNode;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const StyledGridPanel: React.FC<StyledGridPanelProps> = ({
|
|
17
|
+
leftPanel,
|
|
18
|
+
rightPanel,
|
|
19
|
+
children,
|
|
20
|
+
}) => {
|
|
21
|
+
const hasPanels = leftPanel || rightPanel;
|
|
22
|
+
|
|
23
|
+
if (!hasPanels) {
|
|
24
|
+
return <div style={{ width: "100%", height: "100%" }}>{children}</div>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<StyledGrid
|
|
29
|
+
width="100%"
|
|
30
|
+
height="100%"
|
|
31
|
+
gap={0}
|
|
32
|
+
templateColumns="min-content 1fr min-content"
|
|
33
|
+
alignItems="stretch"
|
|
34
|
+
data-testid="styled-grid-panel"
|
|
35
|
+
>
|
|
36
|
+
<StyledBox itemID="left-panel" mr="3" style={{ gridColumn: 1 }}>{leftPanel}</StyledBox>
|
|
37
|
+
<StyledBox style={{ width: "100%", height: "100%", gridColumn: 2 }}>
|
|
38
|
+
{children}
|
|
39
|
+
</StyledBox>
|
|
40
|
+
<StyledBox itemID="right-panel" ml="3" style={{ overflowY: "auto", height: "100%", gridColumn: 3 }}>
|
|
41
|
+
{rightPanel}
|
|
42
|
+
</StyledBox>
|
|
43
|
+
</StyledGrid>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface StyledBoxProps
|
|
48
|
+
extends HTMLStyledProps<"div">,
|
|
49
|
+
BoxRecipeVariantProps {
|
|
50
|
+
as?: React.ElementType;
|
|
51
|
+
leftPanel?: React.ReactNode;
|
|
52
|
+
rightPanel?: React.ReactNode;
|
|
53
|
+
topPanel?: React.ReactNode;
|
|
54
|
+
bottomPanel?: React.ReactNode;
|
|
55
|
+
header?: React.ReactNode;
|
|
56
|
+
footer?: React.ReactNode;
|
|
57
|
+
mt?: ConditionalValue<Property.MarginTop | number>;
|
|
58
|
+
mb?: ConditionalValue<Property.MarginBottom | number>;
|
|
59
|
+
ml?: ConditionalValue<Property.MarginLeft | number>;
|
|
60
|
+
mr?: ConditionalValue<Property.MarginRight | number>;
|
|
61
|
+
p?: ConditionalValue<Property.Padding | number>;
|
|
62
|
+
pt?: ConditionalValue<Property.PaddingTop | number>;
|
|
63
|
+
pb?: ConditionalValue<Property.PaddingBottom | number>;
|
|
64
|
+
pl?: ConditionalValue<Property.PaddingLeft | number>;
|
|
65
|
+
pr?: ConditionalValue<Property.PaddingRight | number>;
|
|
66
|
+
px?: ConditionalValue<Property.Padding | number>;
|
|
67
|
+
py?: ConditionalValue<Property.Padding | number>;
|
|
68
|
+
m?: ConditionalValue<Property.Margin | number>;
|
|
69
|
+
mx?: ConditionalValue<Property.Margin | number>;
|
|
70
|
+
my?: ConditionalValue<Property.Margin | number>;
|
|
71
|
+
width?: ConditionalValue<Property.Width | number>;
|
|
72
|
+
w?: ConditionalValue<Property.Width | number>;
|
|
73
|
+
height?: ConditionalValue<Property.Height | number>;
|
|
74
|
+
h?: ConditionalValue<Property.Height | number>;
|
|
75
|
+
minH?: ConditionalValue<Property.MinHeight | number>;
|
|
76
|
+
maxW?: ConditionalValue<Property.MaxWidth | number>;
|
|
77
|
+
display?: ConditionalValue<Property.Display>;
|
|
78
|
+
flexDirection?: ConditionalValue<Property.FlexDirection>;
|
|
79
|
+
alignItems?: ConditionalValue<Property.AlignItems>;
|
|
80
|
+
justifyContent?: ConditionalValue<Property.JustifyContent>;
|
|
81
|
+
bg?: ConditionalValue<Property.Background>;
|
|
82
|
+
background?: ConditionalValue<Property.Background>;
|
|
83
|
+
color?: ConditionalValue<Property.Color>;
|
|
84
|
+
borderRadius?: ConditionalValue<Property.BorderRadius | number>;
|
|
85
|
+
border?: ConditionalValue<Property.Border | number>;
|
|
86
|
+
borderWidth?: ConditionalValue<Property.BorderWidth | number>;
|
|
87
|
+
borderColor?: ConditionalValue<Property.BorderColor>;
|
|
88
|
+
boxShadow?: ConditionalValue<Property.BoxShadow>;
|
|
89
|
+
position?: ConditionalValue<Property.Position>;
|
|
90
|
+
top?: ConditionalValue<Property.Top | number>;
|
|
91
|
+
right?: ConditionalValue<Property.Right | number>;
|
|
92
|
+
bottom?: ConditionalValue<Property.Bottom | number>;
|
|
93
|
+
left?: ConditionalValue<Property.Left | number>;
|
|
94
|
+
zIndex?: ConditionalValue<Property.ZIndex | number>;
|
|
95
|
+
overflow?: ConditionalValue<Property.Overflow>;
|
|
96
|
+
overflowX?: ConditionalValue<Property.OverflowX>;
|
|
97
|
+
overflowY?: ConditionalValue<Property.OverflowY>;
|
|
98
|
+
textAlign?: ConditionalValue<Property.TextAlign>;
|
|
99
|
+
gap?: ConditionalValue<Property.Gap | number>;
|
|
100
|
+
noWrap?: boolean;
|
|
101
|
+
scrollbar?: "auto" | "on" | "off";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const StyledBoxRoot = styled("div", boxRecipe);
|
|
105
|
+
|
|
106
|
+
const StyledBox = React.forwardRef<HTMLDivElement, StyledBoxProps>(
|
|
107
|
+
({
|
|
108
|
+
as,
|
|
109
|
+
children,
|
|
110
|
+
leftPanel,
|
|
111
|
+
rightPanel,
|
|
112
|
+
topPanel,
|
|
113
|
+
bottomPanel,
|
|
114
|
+
header,
|
|
115
|
+
footer,
|
|
116
|
+
noWrap,
|
|
117
|
+
scrollbar,
|
|
118
|
+
textAlign,
|
|
119
|
+
zIndex,
|
|
120
|
+
className,
|
|
121
|
+
...rest
|
|
122
|
+
},
|
|
123
|
+
ref,
|
|
124
|
+
) => {
|
|
125
|
+
|
|
126
|
+
// Create class for filtered properties
|
|
127
|
+
const filteredPropsClass = css({
|
|
128
|
+
// Only include if defined to avoid overriding with undefined
|
|
129
|
+
...(textAlign && { textAlign }),
|
|
130
|
+
...(zIndex !== undefined && { zIndex })
|
|
131
|
+
});
|
|
132
|
+
const combinedClassName = cx(className, filteredPropsClass);
|
|
133
|
+
|
|
134
|
+
const resolvedHeader = topPanel || header;
|
|
135
|
+
const resolvedFooter = bottomPanel || footer;
|
|
136
|
+
|
|
137
|
+
// scrollbar prop controls inner content overflow
|
|
138
|
+
const innerOverflow: Property.Overflow = scrollbar === "auto" ? "auto" : scrollbar === "on" ? "scroll" : "hidden";
|
|
139
|
+
|
|
140
|
+
if (noWrap) {
|
|
141
|
+
if (resolvedHeader || resolvedFooter) {
|
|
142
|
+
return (
|
|
143
|
+
<StyledBoxRoot ref={ref} {...(as ? { as } : {})} className={combinedClassName} display="flex" flexDirection="column" {...rest}>
|
|
144
|
+
{resolvedHeader}
|
|
145
|
+
<div style={{ flex: 1, minHeight: 0 }}>{children}</div>
|
|
146
|
+
{resolvedFooter}
|
|
147
|
+
</StyledBoxRoot>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
return (
|
|
151
|
+
<StyledBoxRoot ref={ref} {...(as ? { as } : {})} className={combinedClassName} {...rest}>
|
|
152
|
+
{children}
|
|
153
|
+
</StyledBoxRoot>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
return (
|
|
157
|
+
<StyledBoxRoot ref={ref} {...(as ? { as } : {})} className={combinedClassName} {...rest}>
|
|
158
|
+
<StyledVStack gap="0" width="100%" height="100%" style={{ overflow: innerOverflow }}>
|
|
159
|
+
{resolvedHeader}
|
|
160
|
+
<div style={{ flex: 1, minHeight: 0, width: "100%", overflow: innerOverflow }}>
|
|
161
|
+
<StyledGridPanel leftPanel={leftPanel} rightPanel={rightPanel}>
|
|
162
|
+
{children}
|
|
163
|
+
</StyledGridPanel>
|
|
164
|
+
</div>
|
|
165
|
+
{resolvedFooter}
|
|
166
|
+
</StyledVStack>
|
|
167
|
+
</StyledBoxRoot>
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
StyledBox.displayName = "StyledBox";
|
|
173
|
+
export default StyledBox;
|
|
174
|
+
export { StyledBox };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { styled } from "styled-system/jsx";
|
|
5
|
+
import type { HTMLStyledProps } from "styled-system/types";
|
|
6
|
+
import { buttonRecipe } from "styled-system/recipes";
|
|
7
|
+
import type { AllowedVariant } from "../config/types";
|
|
8
|
+
import { useStyleConfig } from "../config/style-config";
|
|
9
|
+
import StyledText from "./StyledText";
|
|
10
|
+
import StyledSpinner from "./StyledSpinner";
|
|
11
|
+
import StyledTooltip from "./StyledTooltip";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The button every other button is built from.
|
|
15
|
+
*
|
|
16
|
+
* ## Why this does NOT use `useResolvedVariant`
|
|
17
|
+
*
|
|
18
|
+
* Everywhere else, `useResolvedVariant` narrows to the five *theme* variants
|
|
19
|
+
* and coerces anything else to `solid`, because most recipes define only those
|
|
20
|
+
* five and passing an unknown one renders an unstyled control.
|
|
21
|
+
*
|
|
22
|
+
* `buttonRecipe` is the exception: it defines **all ten** — the five theme
|
|
23
|
+
* variants plus `ghost`, `none`, `link`, `unstyled` and `selected`. Running the
|
|
24
|
+
* usual narrowing here would silently turn every ghost and link button solid,
|
|
25
|
+
* which is a visible regression at a lot of call sites. So this resolves against
|
|
26
|
+
* the wider `AllowedVariant` set on purpose.
|
|
27
|
+
*
|
|
28
|
+
* If you add a component whose recipe covers all ten, do the same and say why.
|
|
29
|
+
* If it covers only five, use `useResolvedVariant`.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const PandaButton = styled("button", {
|
|
33
|
+
base: {
|
|
34
|
+
display: "inline-flex",
|
|
35
|
+
alignItems: "center",
|
|
36
|
+
justifyContent: "center",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export interface StyledButtonProps extends HTMLStyledProps<"button"> {
|
|
41
|
+
leftIcon?: React.ReactNode;
|
|
42
|
+
rightIcon?: React.ReactNode;
|
|
43
|
+
/** Any of the ten the button recipe defines, not just the five theme ones. */
|
|
44
|
+
variant?: AllowedVariant;
|
|
45
|
+
children?: React.ReactNode;
|
|
46
|
+
/** Disables the button and swaps the label for a spinner. */
|
|
47
|
+
loading?: boolean;
|
|
48
|
+
disabled?: boolean;
|
|
49
|
+
tooltip?: React.ReactNode;
|
|
50
|
+
/** What the spinner says while `loading`. Prefer naming the action. */
|
|
51
|
+
loadText?: React.ReactNode;
|
|
52
|
+
/** Keep the label on one line even when the button is narrow. */
|
|
53
|
+
noWrap?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const StyledButton = React.forwardRef<HTMLButtonElement, StyledButtonProps>(
|
|
57
|
+
function StyledButton(
|
|
58
|
+
{
|
|
59
|
+
leftIcon,
|
|
60
|
+
rightIcon,
|
|
61
|
+
children,
|
|
62
|
+
variant,
|
|
63
|
+
loading,
|
|
64
|
+
disabled,
|
|
65
|
+
tooltip,
|
|
66
|
+
noWrap,
|
|
67
|
+
loadText = "Loading",
|
|
68
|
+
...rest
|
|
69
|
+
},
|
|
70
|
+
ref,
|
|
71
|
+
) {
|
|
72
|
+
const { variant: appVariant } = useStyleConfig();
|
|
73
|
+
const effectiveVariant = variant ?? appVariant;
|
|
74
|
+
|
|
75
|
+
// Positioning props are pulled out of the Panda prop bag and applied as
|
|
76
|
+
// inline style. Panda would otherwise emit them as atomic classes, which
|
|
77
|
+
// lose to the recipe's own class in the cascade — so a caller positioning a
|
|
78
|
+
// button absolutely would find it ignored.
|
|
79
|
+
const { top, right, position, zIndex, ...restWithoutPosition } = rest;
|
|
80
|
+
const style = {
|
|
81
|
+
top,
|
|
82
|
+
right,
|
|
83
|
+
position,
|
|
84
|
+
zIndex,
|
|
85
|
+
...(noWrap ? { whiteSpace: "nowrap" as const } : {}),
|
|
86
|
+
...(rest.style || {}),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<StyledTooltip tooltip={tooltip}>
|
|
91
|
+
<PandaButton
|
|
92
|
+
ref={ref}
|
|
93
|
+
className={buttonRecipe({ variant: effectiveVariant })}
|
|
94
|
+
// A loading button must not be clickable — a second submit is the
|
|
95
|
+
// classic double-charge bug — and `aria-busy` is what tells a screen
|
|
96
|
+
// reader why it went inert.
|
|
97
|
+
disabled={loading || disabled}
|
|
98
|
+
aria-busy={loading}
|
|
99
|
+
data-panda-variant={effectiveVariant}
|
|
100
|
+
style={style as React.CSSProperties}
|
|
101
|
+
{...restWithoutPosition}
|
|
102
|
+
>
|
|
103
|
+
{leftIcon && <IconSlot side="left">{leftIcon}</IconSlot>}
|
|
104
|
+
{loading ? (
|
|
105
|
+
<StyledSpinner loadText={loadText} />
|
|
106
|
+
) : (
|
|
107
|
+
<StyledText>{children}</StyledText>
|
|
108
|
+
)}
|
|
109
|
+
{rightIcon && <IconSlot side="right">{rightIcon}</IconSlot>}
|
|
110
|
+
</PandaButton>
|
|
111
|
+
</StyledTooltip>
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Spacing for an icon beside the label.
|
|
118
|
+
*
|
|
119
|
+
* `em`, not `px`, so the gap tracks the button's own font size — which this
|
|
120
|
+
* system changes app-wide via the font-size profile. A fixed gap looks correct
|
|
121
|
+
* at `md` and wrong at both ends of the scale.
|
|
122
|
+
*/
|
|
123
|
+
const IconSlot = ({
|
|
124
|
+
side,
|
|
125
|
+
children,
|
|
126
|
+
}: {
|
|
127
|
+
side: "left" | "right";
|
|
128
|
+
children: React.ReactNode;
|
|
129
|
+
}) => (
|
|
130
|
+
<span
|
|
131
|
+
style={{
|
|
132
|
+
[side === "left" ? "marginRight" : "marginLeft"]: "0.5em",
|
|
133
|
+
display: "inline-flex",
|
|
134
|
+
alignItems: "center",
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
</span>
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
StyledButton.displayName = "StyledButton";
|
|
142
|
+
|
|
143
|
+
export default StyledButton;
|
|
144
|
+
export { StyledButton };
|