@jasonsdevspace/design-kit 0.1.0-next.1 → 0.1.0-next.2

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.
@@ -0,0 +1,117 @@
1
+ import { type ButtonHTMLAttributes, type InputHTMLAttributes, type ReactNode, type SelectHTMLAttributes } from 'react';
2
+ /** Where the portfolio is, and the section of it that lists the work. */
3
+ export declare const PORTFOLIO_URL = "https://jasonsdevspace.com/";
4
+ export declare const PORTFOLIO_WORK_URL = "https://jasonsdevspace.com/#work";
5
+ /** Where the rest of the work is. */
6
+ export declare const GITHUB_PROFILE_URL = "https://github.com/jasonsdevspace";
7
+ /**
8
+ * The first stop for the keyboard, out of sight until it takes focus, and then at the column's
9
+ * inset. Following it moves focus past the strip to the element whose id it names, the page's main
10
+ * content, which has a tabindex of -1 so it can take focus.
11
+ */
12
+ export declare const SkipLink: ({ target, children, }: {
13
+ /** The id of the element the link leads to. */
14
+ readonly target?: string;
15
+ readonly children?: ReactNode;
16
+ }) => import("react").JSX.Element;
17
+ /**
18
+ * Whose demo this is, above the page, with three links together on the left. It is a navigation
19
+ * landmark in a plain div rather than a header, so the demo's own header stays the page's one
20
+ * banner. A demo with switches of its own puts them beside the links, in a landmark of their own
21
+ * named for them.
22
+ */
23
+ export declare const PortfolioStrip: ({ about, source, current, tools, toolsLabel, }: {
24
+ /** The demo's explainer, relative to the page or absolute. */
25
+ readonly about: string;
26
+ /** The demo's repository. */
27
+ readonly source: string;
28
+ /** The link that is the page the strip sits on, marked as the current page. */
29
+ readonly current?: "about";
30
+ /** The demo's own switches, drawn beside the links. */
31
+ readonly tools?: ReactNode;
32
+ /** The name a screen reader gives the switches' landmark. */
33
+ readonly toolsLabel?: string;
34
+ }) => import("react").JSX.Element;
35
+ /**
36
+ * The exit after a long scroll: whose project this is, how it works, where the source and the rest
37
+ * of the work are, and the license, with anything the demo adds of its own after them.
38
+ */
39
+ export declare const DemoFooter: ({ name, about, source, license, licenseName, children, }: {
40
+ /** The demo's name, as the footer's sentence opens with it. */
41
+ readonly name: string;
42
+ /** The demo's explainer, relative to the page or absolute. */
43
+ readonly about: string;
44
+ /** The demo's repository. */
45
+ readonly source: string;
46
+ /** The demo's license, the LICENSE file on the repository's main branch unless given. */
47
+ readonly license?: string;
48
+ /** What the license is called, as the footer's sentence names it. */
49
+ readonly licenseName?: string;
50
+ /** What the demo adds after the license, as sentences of its own. */
51
+ readonly children?: ReactNode;
52
+ }) => import("react").JSX.Element;
53
+ type NativeButton = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'style' | 'disabled' | 'aria-disabled' | 'aria-pressed'>;
54
+ /**
55
+ * A button's own props: the accent for the one action a view leads with, `pressed` for a toggle
56
+ * that stands alone, and `off`. The accent is never a toggle, so the two do not go together.
57
+ */
58
+ export type ButtonProps = NativeButton & {
59
+ /** Off, but still in the tab order, so a keyboard keeps its place and finds out why. */
60
+ readonly off?: boolean;
61
+ } & ({
62
+ readonly accent: true;
63
+ readonly pressed?: never;
64
+ } | {
65
+ readonly accent?: false;
66
+ /** Whether the toggle is on. False still marks it a toggle, undefined an action. */
67
+ readonly pressed?: boolean;
68
+ });
69
+ /**
70
+ * A button, of type button unless a page gives another, so it never submits a form by surprise.
71
+ * Off, it is marked aria-disabled rather than disabled, since a disabled button that has focus
72
+ * drops it, and a press of it runs no click handler of the page's and stops the form it would
73
+ * submit. The page's other handlers, for keys or a pointer, still run, and check `off` themselves.
74
+ */
75
+ export declare const Button: ({ accent, pressed, off, type, className, onClick, children, ...native }: ButtonProps) => import("react").JSX.Element;
76
+ /** What a field draws around its control. */
77
+ type FieldParts = {
78
+ /** The label, which names the control and focuses it when pressed. */
79
+ readonly label: ReactNode;
80
+ /** What helps a visitor fill the field, read as its description. */
81
+ readonly hint?: ReactNode;
82
+ /** What is wrong with the value, drawn only when given, which marks the control invalid. */
83
+ readonly error?: ReactNode;
84
+ /** The control's id, made with useId unless a page gives one, as it does for a link to it. */
85
+ readonly id?: string;
86
+ };
87
+ export type InputFieldProps = FieldParts & Omit<InputHTMLAttributes<HTMLInputElement>, 'style' | 'id' | 'aria-invalid' | 'children'>;
88
+ /** A text input with its label, hint and error, the input taking every other prop given. */
89
+ export declare const InputField: (props: InputFieldProps) => import("react").JSX.Element;
90
+ export type SelectFieldProps = FieldParts & Omit<SelectHTMLAttributes<HTMLSelectElement>, 'style' | 'id' | 'aria-invalid'>;
91
+ /** A native select with its label, hint and error, its children the options. */
92
+ export declare const SelectField: (props: SelectFieldProps) => import("react").JSX.Element;
93
+ /** What a callout is about: news, something that went right, or something that went wrong. */
94
+ export type CalloutTone = 'info' | 'success' | 'danger';
95
+ /**
96
+ * A message set apart from the page, with an optional title before it and actions after it. It has
97
+ * no role unless the page gives `live`, whatever its tone, since a message on the page when it
98
+ * loads is read in its place. A live message is drawn even when empty, so the page can mount it
99
+ * first and fill it after, which is when a screen reader reads it. The role sits on the message,
100
+ * which holds the title and the tone's word too, so they are read with it, and the actions are
101
+ * not.
102
+ */
103
+ export declare const Callout: ({ tone, title, headingLevel, live, prefix, actions, children, }: {
104
+ readonly tone?: CalloutTone;
105
+ /** What the callout is about, drawn before its message. */
106
+ readonly title?: ReactNode;
107
+ /** The title's heading level, where the callout is a section a visitor would jump to. */
108
+ readonly headingLevel?: 2 | 3 | 4 | 5 | 6;
109
+ /** A message a screen reader reads when it changes, politely or at once. */
110
+ readonly live?: "status" | "alert";
111
+ /** The word read before the title or the message, the tone's own unless given. */
112
+ readonly prefix?: string;
113
+ /** Buttons or links that act on the message. */
114
+ readonly actions?: ReactNode;
115
+ readonly children?: ReactNode;
116
+ }) => import("react").JSX.Element;
117
+ export {};
package/dist/react.js ADDED
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useId, } from 'react';
3
+ /*
4
+ * The page chrome a demo draws around its page, a skip link, a strip above the page that says
5
+ * whose demo it is and a footer below it, and the buttons, fields and callouts on the page itself.
6
+ * Each component draws the markup components.css styles and nothing else, with no stylesheet of
7
+ * its own and no style attribute, so a page without React writes the same markup by hand, and a
8
+ * page whose policy allows styles only from its own origin draws it. A control's state is a
9
+ * native or ARIA attribute, never a class of the kit's.
10
+ */
11
+ /** Whether a slot holds something to draw: a demo's `cond && <X />` leaves false or null. */
12
+ const drawn = (node) => node !== undefined && node !== null && node !== false;
13
+ /** Where the portfolio is, and the section of it that lists the work. */
14
+ export const PORTFOLIO_URL = 'https://jasonsdevspace.com/';
15
+ export const PORTFOLIO_WORK_URL = 'https://jasonsdevspace.com/#work';
16
+ /** Where the rest of the work is. */
17
+ export const GITHUB_PROFILE_URL = 'https://github.com/jasonsdevspace';
18
+ /**
19
+ * The first stop for the keyboard, out of sight until it takes focus, and then at the column's
20
+ * inset. Following it moves focus past the strip to the element whose id it names, the page's main
21
+ * content, which has a tabindex of -1 so it can take focus.
22
+ */
23
+ export const SkipLink = ({ target = 'main-content', children = 'Skip to content', }) => (_jsx("a", { className: "dk-skip", href: `#${target}`, children: children }));
24
+ /**
25
+ * Whose demo this is, above the page, with three links together on the left. It is a navigation
26
+ * landmark in a plain div rather than a header, so the demo's own header stays the page's one
27
+ * banner. A demo with switches of its own puts them beside the links, in a landmark of their own
28
+ * named for them.
29
+ */
30
+ export const PortfolioStrip = ({ about, source, current, tools, toolsLabel = 'Demo', }) => (_jsxs("div", { className: "dk-strip", children: [_jsxs("nav", { className: "dk-strip-links", "aria-label": "Portfolio", children: [_jsx("a", { href: PORTFOLIO_WORK_URL, children: "A demo by Jason Poteet" }), _jsx("a", { href: about, "aria-current": current === 'about' ? 'page' : undefined, children: "How it works" }), _jsx("a", { href: source, children: "Source on GitHub" })] }), !drawn(tools) ? null : (_jsx("section", { className: "dk-strip-tools", "aria-label": toolsLabel, children: tools }))] }));
31
+ /**
32
+ * The exit after a long scroll: whose project this is, how it works, where the source and the rest
33
+ * of the work are, and the license, with anything the demo adds of its own after them.
34
+ */
35
+ export const DemoFooter = ({ name, about, source, license = `${source.endsWith('/') ? source.slice(0, -1) : source}/blob/main/LICENSE`, licenseName = 'MIT license', children, }) => (_jsx("footer", { className: "dk-footer", children: _jsxs("p", { children: [name, " is a portfolio project by ", _jsx("a", { href: PORTFOLIO_URL, children: "Jason Poteet" }), ".", ' ', _jsx("a", { href: about, children: "How it works" }), ", in plain words. The ", _jsx("a", { href: source, children: "source" }), " and", ' ', _jsx("a", { href: GITHUB_PROFILE_URL, children: "more work" }), " are on GitHub, under the", ' ', _jsx("a", { href: license, children: licenseName }), ".", drawn(children) ? _jsxs(_Fragment, { children: [" ", children] }) : null] }) }));
36
+ /** Whether a slot holds words to draw, an empty string being none. */
37
+ const filled = (node) => drawn(node) && node !== '';
38
+ /**
39
+ * A list of names an attribute holds, such as the classes a component draws, its own first and a
40
+ * page's after them, or the ids that describe a control, with any left out or empty dropped.
41
+ */
42
+ const spaced = (...names) => names.filter((name) => typeof name === 'string' && name !== '').join(' ');
43
+ /**
44
+ * A button, of type button unless a page gives another, so it never submits a form by surprise.
45
+ * Off, it is marked aria-disabled rather than disabled, since a disabled button that has focus
46
+ * drops it, and a press of it runs no click handler of the page's and stops the form it would
47
+ * submit. The page's other handlers, for keys or a pointer, still run, and check `off` themselves.
48
+ */
49
+ export const Button = ({ accent, pressed, off = false, type = 'button', className, onClick, children, ...native }) => (_jsx("button", { ...native, type: type, className: spaced('dk-button', accent === true && 'dk-button-accent', className), "aria-pressed": pressed, "aria-disabled": off ? true : undefined, onClick: off
50
+ ? (event) => {
51
+ event.preventDefault();
52
+ }
53
+ : onClick, children: children }));
54
+ /**
55
+ * The attributes that tie a control to its label, hint and error. Only what is drawn is listed,
56
+ * the hint before the error as they are read, then any description the page gives of its own.
57
+ */
58
+ const useField = ({ id, hint, error }, own) => {
59
+ const made = useId();
60
+ // An empty id would tie nothing, so it is made as one left out is.
61
+ const control = id === undefined || id === '' ? made : id;
62
+ const hinted = filled(hint);
63
+ const wrong = filled(error);
64
+ const describedBy = spaced(hinted && `${control}-hint`, wrong && `${control}-error`, own);
65
+ return {
66
+ id: control,
67
+ hint: hinted,
68
+ error: wrong,
69
+ wired: {
70
+ id: control,
71
+ 'aria-describedby': describedBy === '' ? undefined : describedBy,
72
+ 'aria-invalid': wrong ? true : undefined,
73
+ },
74
+ };
75
+ };
76
+ /**
77
+ * A field's label, hint and error around its control. The error sits above the control, so a
78
+ * phone's keyboard never covers it, and starts with a word a screen reader reads, so the error is
79
+ * told from the hint by more than its color. It is not live, since a form that fails in several
80
+ * fields at once would speak over itself, so the page says when an error appears.
81
+ */
82
+ const Field = ({ parts, field, children, }) => (_jsxs("div", { className: "dk-field", children: [_jsx("label", { className: "dk-field-label", htmlFor: field.id, children: parts.label }), field.hint ? (_jsx("p", { className: "dk-field-hint", id: `${field.id}-hint`, children: parts.hint })) : null, field.error ? (_jsxs("p", { className: "dk-field-error", id: `${field.id}-error`, children: [_jsx("span", { className: "dk-visually-hidden", children: "Error: " }), parts.error] })) : null, children] }));
83
+ /** A text input with its label, hint and error, the input taking every other prop given. */
84
+ export const InputField = (props) => {
85
+ const { label: _label, hint: _hint, error: _error, id: _id, className, ...rest } = props;
86
+ const { 'aria-describedby': own, ...native } = rest;
87
+ const field = useField(props, own);
88
+ return (_jsx(Field, { parts: props, field: field, children: _jsx("input", { className: spaced('dk-input', className), ...native, ...field.wired }) }));
89
+ };
90
+ /** A native select with its label, hint and error, its children the options. */
91
+ export const SelectField = (props) => {
92
+ const { label: _label, hint: _hint, error: _error, id: _id, className, ...rest } = props;
93
+ const { 'aria-describedby': own, children, ...native } = rest;
94
+ const field = useField(props, own);
95
+ return (_jsx(Field, { parts: props, field: field, children: _jsx("select", { className: spaced('dk-select', className), ...native, ...field.wired, children: children }) }));
96
+ };
97
+ /** The word a screen reader reads before each tone's message, so the tone is not color alone. */
98
+ const TONE_WORDS = {
99
+ info: '',
100
+ success: 'Success: ',
101
+ danger: 'Error: ',
102
+ };
103
+ /** The element a callout's title is drawn as, at each heading level a page may give it. */
104
+ const HEADINGS = { 2: 'h2', 3: 'h3', 4: 'h4', 5: 'h5', 6: 'h6' };
105
+ /**
106
+ * A message set apart from the page, with an optional title before it and actions after it. It has
107
+ * no role unless the page gives `live`, whatever its tone, since a message on the page when it
108
+ * loads is read in its place. A live message is drawn even when empty, so the page can mount it
109
+ * first and fill it after, which is when a screen reader reads it. The role sits on the message,
110
+ * which holds the title and the tone's word too, so they are read with it, and the actions are
111
+ * not.
112
+ */
113
+ export const Callout = ({ tone = 'info', title, headingLevel, live, prefix = TONE_WORDS[tone], actions, children, }) => {
114
+ // A level the types refuse, as a page in plain JavaScript can pass, draws a paragraph.
115
+ const Title = (headingLevel === undefined ? undefined : HEADINGS[headingLevel]) ?? 'p';
116
+ const titled = filled(title);
117
+ const said = filled(children);
118
+ const word = prefix === '' ? null : _jsx("span", { className: "dk-visually-hidden", children: prefix });
119
+ return (_jsxs("div", { className: spaced('dk-callout', tone !== 'info' && `dk-callout-${tone}`), children: [titled || said || live !== undefined ? (_jsxs("div", { className: "dk-callout-body", role: live, children: [titled ? (_jsxs(Title, { className: "dk-callout-title", children: [word, title] })) : null, !titled && said ? word : null, said ? children : null] })) : null, filled(actions) ? _jsx("div", { className: "dk-callout-actions", children: actions }) : null] }));
120
+ };
package/dist/tokens.css CHANGED
@@ -1,26 +1,27 @@
1
1
  /*
2
2
  * The kit's named values: the scale a page's styles are drawn from, and the color roles a demo
3
- * fills with its own palette. Every name carries the dk prefix, so a page that loads the kit
4
- * beside its own styles keeps its own names.
3
+ * fills with its own palette. Every name carries the dk prefix, so a page that loads the kit beside
4
+ * its own styles keeps its own names.
5
5
  *
6
- * Each color role holds its light and its dark value in one light-dark(), which picks by the
7
- * color scheme of the element it is drawn on. The scheme follows data-theme on the root when a
8
- * page sets it, as the portfolio does with the theme a visitor saved, and the system's otherwise.
9
- * A demo with its own palette sets the roles again on its root, both values at once.
6
+ * Each color role holds its light and its dark value in one light-dark(), which picks by the color
7
+ * scheme of the element it is drawn on. The scheme follows data-theme on the root when a page sets
8
+ * it, as the portfolio does with the theme a visitor saved, and the system's otherwise. A demo with
9
+ * its own palette sets the roles again on its root, both values at once.
10
10
  *
11
11
  * Every rule in the kit sits in one of its layers, declared here in order, so a page's own rule
12
12
  * outside a layer wins over the kit's, the cut to motion in the base aside, and a page that names
13
13
  * dk among layers of its own sets where dk goes. A demo's own :root outside a layer wins the same
14
- * way, whichever sheet loads first. Across the kit, every selector but the visually hidden class is
15
- * one :where(), followed at most by a focus pseudo-class, and here each is a :where() alone, so
16
- * nothing in the tokens carries weight.
14
+ * way, whichever sheet loads first. Across the kit, every selector is one of the kit's own classes
15
+ * or one :where(), followed at most by a focus pseudo-class or, in the base alone, a
16
+ * pseudo-element, and here each is a :where() alone, so nothing in the tokens carries weight.
17
17
  *
18
18
  * Every text pairing the palette is meant for passes 4.5 to 1 in both themes, and a control's edge
19
19
  * 3 to 1 against what it sits on. The focus ring is in the accent, held at 4.5 to 1 as a link. A
20
20
  * test holds the palette to both.
21
21
  *
22
- * Which unit goes where: rem for anything that should grow with the visitor's own text size, and
23
- * px for what must not scale, a border, the focus ring and a shadow.
22
+ * Which unit goes where: rem for anything that should grow with the visitor's own text size, em for
23
+ * code set in running text, which sizes to the text around it, and px for what must not scale, a
24
+ * border, the focus ring and a shadow.
24
25
  */
25
26
 
26
27
  @layer dk.tokens, dk.base, dk.components;
@@ -43,7 +44,7 @@
43
44
 
44
45
  /* The lines between things, and the stronger edge of a field or button a visitor uses. */
45
46
  --dk-line: light-dark(#d9d9d9, #3a3a3a);
46
- --dk-control: light-dark(#858585, #7a7a7a);
47
+ --dk-control: light-dark(#858585, #7d7d7d);
47
48
 
48
49
  /* Links, the focus ring and the one action a page leads with, and the text drawn on it. */
49
50
  --dk-accent: light-dark(#1f4fd8, #8fb0ff);
@@ -73,6 +74,8 @@
73
74
  --dk-text-title: 1.25rem;
74
75
  --dk-text-heading: 1.5rem;
75
76
  --dk-text-display: 1.75rem;
77
+ /* Code, a step under the text around it, since mono type looks larger at the same size. */
78
+ --dk-text-code: 0.9em;
76
79
  --dk-leading-tight: 1.2;
77
80
  --dk-leading-body: 1.5;
78
81
  --dk-weight-normal: 400;
@@ -98,8 +101,11 @@
98
101
  /* The height of the portfolio strip's row, and of each link in it. */
99
102
  --dk-strip: 2.5rem;
100
103
 
101
- /* The width a page of reading holds to, and the widest a page's column grows. */
102
- --dk-measure: 42rem;
104
+ /*
105
+ * The width a page of reading holds to, no more than 80 characters a line in the body size,
106
+ * and the widest a page's column grows.
107
+ */
108
+ --dk-measure: 34rem;
103
109
  --dk-column: 72rem;
104
110
 
105
111
  /* What floats over the page, and what dims the page behind it, darker on a dark page. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jasonsdevspace/design-kit",
3
- "version": "0.1.0-next.1",
4
- "description": "Design tokens, base styles and the theme script for the demos on jasonsdevspace.com.",
3
+ "version": "0.1.0-next.2",
4
+ "description": "Design tokens, base styles, a theme script, page chrome, buttons, fields, callouts and an explainer layout for the demos on jasonsdevspace.com.",
5
5
  "keywords": [
6
6
  "design-tokens",
7
7
  "design-system",
@@ -10,7 +10,8 @@
10
10
  "dark-mode",
11
11
  "light-dark",
12
12
  "theme",
13
- "vite-plugin"
13
+ "vite-plugin",
14
+ "react"
14
15
  ],
15
16
  "license": "MIT",
16
17
  "author": "Jason Poteet",
@@ -27,10 +28,15 @@
27
28
  "exports": {
28
29
  "./tokens.css": "./dist/tokens.css",
29
30
  "./base.css": "./dist/base.css",
31
+ "./components.css": "./dist/components.css",
30
32
  "./theme": {
31
33
  "types": "./dist/theme.d.ts",
32
34
  "default": "./dist/theme.js"
33
35
  },
36
+ "./react": {
37
+ "types": "./dist/react.d.ts",
38
+ "default": "./dist/react.js"
39
+ },
34
40
  "./vite": {
35
41
  "types": "./dist/vite.d.ts",
36
42
  "default": "./dist/vite.js"
@@ -55,9 +61,13 @@
55
61
  "prepack": "npm run build"
56
62
  },
57
63
  "peerDependencies": {
64
+ "react": "^19.3.0",
58
65
  "vite": ">=8"
59
66
  },
60
67
  "peerDependenciesMeta": {
68
+ "react": {
69
+ "optional": true
70
+ },
61
71
  "vite": {
62
72
  "optional": true
63
73
  }