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

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,137 @@
1
+ import { type ComponentPropsWithRef, type ReactNode } 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
+ /** A page of the demo's that the chrome links to, and can mark as the one it sits on. */
18
+ type ChromePage = 'about';
19
+ /**
20
+ * Whose demo this is, above the page, with three links together on the left. It is a navigation
21
+ * landmark in a plain div rather than a header, so the demo's own header stays the page's one
22
+ * banner. A demo with switches of its own puts them beside the links, in a landmark of their own
23
+ * named for them, and while they are on their way the strip keeps their room in that section,
24
+ * unnamed and so no landmark.
25
+ */
26
+ export declare const PortfolioStrip: ({ about, source, current, tools, toolsLabel, toolsWaiting, }: {
27
+ /** The demo's explainer, relative to the page or absolute. */
28
+ readonly about: string;
29
+ /** The demo's repository. */
30
+ readonly source: string;
31
+ /** The link that is the page the strip sits on, marked as the current page. */
32
+ readonly current?: ChromePage;
33
+ /** The demo's own switches, drawn beside the links. */
34
+ readonly tools?: ReactNode;
35
+ /** The name a screen reader gives the switches' landmark. */
36
+ readonly toolsLabel?: string;
37
+ /**
38
+ * The switches are on their way, so the strip keeps the room of one row of the kit's controls
39
+ * in their section, empty and unnamed, so no landmark, until `tools` holds them and the section
40
+ * takes its name.
41
+ */
42
+ readonly toolsWaiting?: boolean;
43
+ }) => import("react").JSX.Element;
44
+ /**
45
+ * The exit after a long scroll: whose project this is, how it works, where the source and the rest
46
+ * of the work are, and the license, with anything the demo adds of its own after them.
47
+ */
48
+ export declare const DemoFooter: ({ name, about, source, current, license, licenseName, children, }: {
49
+ /** The demo's name, as the footer's sentence opens with it. */
50
+ readonly name: string;
51
+ /** The demo's explainer, relative to the page or absolute. */
52
+ readonly about: string;
53
+ /** The demo's repository. */
54
+ readonly source: string;
55
+ /** The link that is the page the footer sits on, marked as the current page. */
56
+ readonly current?: ChromePage;
57
+ /** The demo's license, the LICENSE file on the repository's main branch unless given. */
58
+ readonly license?: string;
59
+ /** What the license is called, as the footer's sentence names it. */
60
+ readonly licenseName?: string;
61
+ /** What the demo adds after the license, as sentences of its own. */
62
+ readonly children?: ReactNode;
63
+ }) => import("react").JSX.Element;
64
+ type NativeButton = Omit<ComponentPropsWithRef<'button'>, 'style' | 'disabled' | 'aria-disabled' | 'aria-pressed'>;
65
+ /**
66
+ * A button's own props: the accent for the one action a view leads with, `pressed` for a toggle
67
+ * that stands alone, and `off`, beside a `<button>`'s own, its `ref` among them, which holds the
68
+ * button drawn for a page that moves focus to it. The accent is never a toggle, so the two do not
69
+ * go together.
70
+ */
71
+ export type ButtonProps = NativeButton & {
72
+ /** Off, but still in the tab order, so a keyboard keeps its place and finds out why. */
73
+ readonly off?: boolean;
74
+ } & ({
75
+ readonly accent: true;
76
+ readonly pressed?: never;
77
+ } | {
78
+ readonly accent?: false;
79
+ /** Whether the toggle is on. False still marks it a toggle, undefined an action. */
80
+ readonly pressed?: boolean;
81
+ });
82
+ /**
83
+ * A button, of type button unless a page gives another, so it never submits a form by surprise.
84
+ * Off, it is marked aria-disabled rather than disabled, since a disabled button that has focus
85
+ * drops it, and a press of it runs no click handler of the page's and stops the form it would
86
+ * submit. The page's other handlers, for keys or a pointer, still run, and check `off` themselves.
87
+ */
88
+ export declare const Button: ({ accent, pressed, off, type, className, onClick, children, ...native }: ButtonProps) => import("react").JSX.Element;
89
+ /** What a field draws around its control. */
90
+ type FieldParts = {
91
+ /** The label, which names the control and focuses it when pressed. */
92
+ readonly label: ReactNode;
93
+ /** What helps a visitor fill the field, read as its description. */
94
+ readonly hint?: ReactNode;
95
+ /** What is wrong with the value, drawn only when given, which marks the control invalid. */
96
+ readonly error?: ReactNode;
97
+ /** The control's id, made with useId unless a page gives one, as it does for a link to it. */
98
+ readonly id?: string;
99
+ };
100
+ export type InputFieldProps = FieldParts & Omit<ComponentPropsWithRef<'input'>, 'style' | 'id' | 'aria-invalid' | 'children'>;
101
+ /**
102
+ * A text input with its label, hint and error, the input taking every other prop given, its `ref`
103
+ * among them, which holds the input drawn for a page that moves focus to it.
104
+ */
105
+ export declare const InputField: (props: InputFieldProps) => import("react").JSX.Element;
106
+ export type SelectFieldProps = FieldParts & Omit<ComponentPropsWithRef<'select'>, 'style' | 'id' | 'aria-invalid'>;
107
+ /**
108
+ * A native select with its label, hint and error, its children the options, and the select taking
109
+ * every other prop given, its `ref` among them, which holds the select drawn for a page that moves
110
+ * focus to it.
111
+ */
112
+ export declare const SelectField: (props: SelectFieldProps) => import("react").JSX.Element;
113
+ /** What a callout is about: news, something that went right, or something that went wrong. */
114
+ export type CalloutTone = 'info' | 'success' | 'danger';
115
+ /**
116
+ * A message set apart from the page, with an optional title before it and actions after it. It has
117
+ * no role unless the page gives `live`, whatever its tone, since a message on the page when it
118
+ * loads is read in its place. A live message is drawn even when empty, so the page can mount it
119
+ * first and fill it after, which is when a screen reader reads it. The role sits on the message,
120
+ * which holds the title and the tone's word too, so they are read with it, and the actions are
121
+ * not.
122
+ */
123
+ export declare const Callout: ({ tone, title, headingLevel, live, prefix, actions, children, }: {
124
+ readonly tone?: CalloutTone;
125
+ /** What the callout is about, drawn before its message. */
126
+ readonly title?: ReactNode;
127
+ /** The title's heading level, where the callout is a section a visitor would jump to. */
128
+ readonly headingLevel?: 2 | 3 | 4 | 5 | 6;
129
+ /** A message a screen reader reads when it changes, politely or at once. */
130
+ readonly live?: "status" | "alert";
131
+ /** The word read before the title or the message, the tone's own unless given. */
132
+ readonly prefix?: string;
133
+ /** Buttons or links that act on the message. */
134
+ readonly actions?: ReactNode;
135
+ readonly children?: ReactNode;
136
+ }) => import("react").JSX.Element;
137
+ export {};
package/dist/react.js ADDED
@@ -0,0 +1,130 @@
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
+ /** The mark a link to `page` takes when it is the page the chrome sits on, and none otherwise. */
25
+ const currentAs = (current, page) => current === page ? 'page' : undefined;
26
+ /**
27
+ * Whose demo this is, above the page, with three links together on the left. It is a navigation
28
+ * landmark in a plain div rather than a header, so the demo's own header stays the page's one
29
+ * banner. A demo with switches of its own puts them beside the links, in a landmark of their own
30
+ * named for them, and while they are on their way the strip keeps their room in that section,
31
+ * unnamed and so no landmark.
32
+ */
33
+ export const PortfolioStrip = ({ about, source, current, tools, toolsLabel = 'Demo', toolsWaiting = false, }) => (_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": currentAs(current, 'about'), children: "How it works" }), _jsx("a", { href: source, children: "Source on GitHub" })] }), drawn(tools) ? (_jsx("section", { className: "dk-strip-tools", "aria-label": toolsLabel, children: tools })) : toolsWaiting ? (_jsx("section", { className: "dk-strip-tools", children: _jsx("span", { className: "dk-strip-room" }) })) : null] }));
34
+ /**
35
+ * The exit after a long scroll: whose project this is, how it works, where the source and the rest
36
+ * of the work are, and the license, with anything the demo adds of its own after them.
37
+ */
38
+ export const DemoFooter = ({ name, about, source, current, 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, "aria-current": currentAs(current, '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] }) }));
39
+ /** Whether a slot holds words to draw, an empty string being none. */
40
+ const filled = (node) => drawn(node) && node !== '';
41
+ /**
42
+ * A list of names an attribute holds, such as the classes a component draws, its own first and a
43
+ * page's after them, or the ids that describe a control, with any left out or empty dropped.
44
+ */
45
+ const spaced = (...names) => names.filter((name) => typeof name === 'string' && name !== '').join(' ');
46
+ /**
47
+ * A button, of type button unless a page gives another, so it never submits a form by surprise.
48
+ * Off, it is marked aria-disabled rather than disabled, since a disabled button that has focus
49
+ * drops it, and a press of it runs no click handler of the page's and stops the form it would
50
+ * submit. The page's other handlers, for keys or a pointer, still run, and check `off` themselves.
51
+ */
52
+ 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
53
+ ? (event) => {
54
+ event.preventDefault();
55
+ }
56
+ : onClick, children: children }));
57
+ /**
58
+ * The attributes that tie a control to its label, hint and error. Only what is drawn is listed,
59
+ * the hint before the error as they are read, then any description the page gives of its own.
60
+ */
61
+ const useField = ({ id, hint, error }, own) => {
62
+ const made = useId();
63
+ // An empty id would tie nothing, so it is made as one left out is.
64
+ const control = id === undefined || id === '' ? made : id;
65
+ const hinted = filled(hint);
66
+ const wrong = filled(error);
67
+ const describedBy = spaced(hinted && `${control}-hint`, wrong && `${control}-error`, own);
68
+ return {
69
+ id: control,
70
+ hint: hinted,
71
+ error: wrong,
72
+ wired: {
73
+ id: control,
74
+ 'aria-describedby': describedBy === '' ? undefined : describedBy,
75
+ 'aria-invalid': wrong ? true : undefined,
76
+ },
77
+ };
78
+ };
79
+ /**
80
+ * A field's label, hint and error around its control. The error sits above the control, so a
81
+ * phone's keyboard never covers it, and starts with a word a screen reader reads, so the error is
82
+ * told from the hint by more than its color. It is not live, since a form that fails in several
83
+ * fields at once would speak over itself, so the page says when an error appears.
84
+ */
85
+ 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] }));
86
+ /**
87
+ * A text input with its label, hint and error, the input taking every other prop given, its `ref`
88
+ * among them, which holds the input drawn for a page that moves focus to it.
89
+ */
90
+ export const InputField = (props) => {
91
+ const { label: _label, hint: _hint, error: _error, id: _id, className, ...rest } = props;
92
+ const { 'aria-describedby': own, ...native } = rest;
93
+ const field = useField(props, own);
94
+ return (_jsx(Field, { parts: props, field: field, children: _jsx("input", { className: spaced('dk-input', className), ...native, ...field.wired }) }));
95
+ };
96
+ /**
97
+ * A native select with its label, hint and error, its children the options, and the select taking
98
+ * every other prop given, its `ref` among them, which holds the select drawn for a page that moves
99
+ * focus to it.
100
+ */
101
+ export const SelectField = (props) => {
102
+ const { label: _label, hint: _hint, error: _error, id: _id, className, ...rest } = props;
103
+ const { 'aria-describedby': own, children, ...native } = rest;
104
+ const field = useField(props, own);
105
+ return (_jsx(Field, { parts: props, field: field, children: _jsx("select", { className: spaced('dk-select', className), ...native, ...field.wired, children: children }) }));
106
+ };
107
+ /** The word a screen reader reads before each tone's message, so the tone is not color alone. */
108
+ const TONE_WORDS = {
109
+ info: '',
110
+ success: 'Success: ',
111
+ danger: 'Error: ',
112
+ };
113
+ /** The element a callout's title is drawn as, at each heading level a page may give it. */
114
+ const HEADINGS = { 2: 'h2', 3: 'h3', 4: 'h4', 5: 'h5', 6: 'h6' };
115
+ /**
116
+ * A message set apart from the page, with an optional title before it and actions after it. It has
117
+ * no role unless the page gives `live`, whatever its tone, since a message on the page when it
118
+ * loads is read in its place. A live message is drawn even when empty, so the page can mount it
119
+ * first and fill it after, which is when a screen reader reads it. The role sits on the message,
120
+ * which holds the title and the tone's word too, so they are read with it, and the actions are
121
+ * not.
122
+ */
123
+ export const Callout = ({ tone = 'info', title, headingLevel, live, prefix = TONE_WORDS[tone], actions, children, }) => {
124
+ // A level the types refuse, as a page in plain JavaScript can pass, draws a paragraph.
125
+ const Title = (headingLevel === undefined ? undefined : HEADINGS[headingLevel]) ?? 'p';
126
+ const titled = filled(title);
127
+ const said = filled(children);
128
+ const word = prefix === '' ? null : _jsx("span", { className: "dk-visually-hidden", children: prefix });
129
+ 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] }));
130
+ };
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.3",
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
  }