@themodcraft/core-ui 0.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +16 -0
  2. package/dist/components/button/Button.d.ts +1 -1
  3. package/dist/components/button/Button.js +119 -17
  4. package/dist/components/button/Button.types.d.ts +6 -1
  5. package/dist/components/button/index.d.ts +1 -1
  6. package/dist/components/checkbox/Checkbox.d.ts +2 -0
  7. package/dist/components/checkbox/Checkbox.js +47 -0
  8. package/dist/components/checkbox/Checkbox.types.d.ts +5 -0
  9. package/dist/components/checkbox/Checkbox.types.js +1 -0
  10. package/dist/components/checkbox/Switch.d.ts +2 -0
  11. package/dist/components/checkbox/Switch.js +79 -0
  12. package/dist/components/checkbox/index.d.ts +3 -0
  13. package/dist/components/checkbox/index.js +2 -0
  14. package/dist/components/code-block/CodeBlock.d.ts +2 -0
  15. package/dist/components/code-block/CodeBlock.js +56 -0
  16. package/dist/components/code-block/CodeBlock.types.d.ts +6 -0
  17. package/dist/components/code-block/CodeBlock.types.js +1 -0
  18. package/dist/components/code-block/index.d.ts +2 -0
  19. package/dist/components/code-block/index.js +1 -0
  20. package/dist/components/divider/Divider.d.ts +2 -0
  21. package/dist/components/divider/Divider.js +36 -0
  22. package/dist/components/divider/Divider.types.d.ts +5 -0
  23. package/dist/components/divider/Divider.types.js +1 -0
  24. package/dist/components/divider/index.d.ts +2 -0
  25. package/dist/components/divider/index.js +1 -0
  26. package/dist/components/radio/Radio.d.ts +3 -0
  27. package/dist/components/radio/Radio.js +79 -0
  28. package/dist/components/radio/Radio.types.d.ts +20 -0
  29. package/dist/components/radio/Radio.types.js +1 -0
  30. package/dist/components/radio/index.d.ts +2 -0
  31. package/dist/components/radio/index.js +1 -0
  32. package/dist/components/slider/Slider.d.ts +2 -0
  33. package/dist/components/slider/Slider.js +86 -0
  34. package/dist/components/slider/Slider.types.d.ts +6 -0
  35. package/dist/components/slider/Slider.types.js +1 -0
  36. package/dist/components/slider/index.d.ts +2 -0
  37. package/dist/components/slider/index.js +1 -0
  38. package/dist/components/text-input/AuthFields.d.ts +6 -0
  39. package/dist/components/text-input/AuthFields.js +15 -0
  40. package/dist/components/text-input/TextArea.d.ts +2 -0
  41. package/dist/components/text-input/TextArea.js +18 -0
  42. package/dist/components/text-input/TextInput.d.ts +2 -0
  43. package/dist/components/text-input/TextInput.js +14 -0
  44. package/dist/components/text-input/TextInput.types.d.ts +11 -0
  45. package/dist/components/text-input/TextInput.types.js +1 -0
  46. package/dist/components/text-input/index.d.ts +5 -0
  47. package/dist/components/text-input/index.js +3 -0
  48. package/dist/components/text-input/shared.d.ts +26 -0
  49. package/dist/components/text-input/shared.js +113 -0
  50. package/dist/index.d.ts +7 -0
  51. package/dist/index.js +7 -0
  52. package/dist/styles/index.css +55 -7
  53. package/dist/utils/color.d.ts +6 -0
  54. package/dist/utils/color.js +66 -0
  55. package/package.json +5 -1
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # `@themodcraft/core-ui`
2
+
3
+ Base UI package for the library.
4
+
5
+ Current scope:
6
+ - Button, checkbox, switch, radio, divider, auth fields, text input, text area, code block, and range slider primitives
7
+ - Shared config helpers
8
+ - Asset path mapping
9
+ - Core theme tokens and styles
10
+
11
+ Install:
12
+ ```bash
13
+ npm install @themodcraft/core-ui
14
+ ```
15
+
16
+ This package publishes compiled output only.
@@ -1,2 +1,2 @@
1
1
  import type { ButtonProps } from "./Button.types";
2
- export declare function Button({ className, size, tone, type, ...props }: ButtonProps): import("react").JSX.Element;
2
+ export declare function Button({ active, btype, className, labeling, onClick, onMouseOver, size, tone, buttonStyleType, style, type, children, ...props }: ButtonProps): import("react").JSX.Element;
@@ -1,21 +1,123 @@
1
+ "use client";
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
2
- const baseClasses = "inline-flex items-center justify-center rounded-md border font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50";
3
- const sizeClasses = {
4
- sm: "h-8 px-3 text-sm",
5
- md: "h-10 px-4 text-sm",
3
+ import styled from "@emotion/styled";
4
+ import { darken, getReadableTextColor } from "../../utils/color";
5
+ const toneToStyleType = {
6
+ neutral: "normal",
7
+ accent: "submit",
8
+ danger: "danger",
6
9
  };
7
- const toneClasses = {
8
- neutral: "border-zinc-300 bg-white text-zinc-950 hover:bg-zinc-100 focus-visible:outline-zinc-500",
9
- accent: "border-blue-700 bg-blue-700 text-white hover:bg-blue-800 focus-visible:outline-amber-400",
10
- danger: "border-red-700 bg-red-700 text-white hover:bg-red-800 focus-visible:outline-amber-400",
10
+ const sizeStyles = {
11
+ sm: {
12
+ fontSize: "0.875rem",
13
+ minHeight: "2rem",
14
+ padding: "0 0.75rem",
15
+ },
16
+ md: {
17
+ fontSize: "0.95rem",
18
+ minHeight: "5vh",
19
+ padding: "0 1rem",
20
+ },
11
21
  };
12
- export function Button({ className, size = "md", tone = "neutral", type = "button", ...props }) {
13
- return (_jsx("button", { className: [
14
- baseClasses,
15
- sizeClasses[size],
16
- toneClasses[tone],
17
- className,
18
- ]
19
- .filter(Boolean)
20
- .join(" "), type: type, ...props }));
22
+ const StyledButton = styled.button `
23
+ --button-color: var(--tmc-color-surface-raised, #fbfcfe);
24
+ --button-text-color: var(--tmc-color-text, #151821);
25
+ --button-shadow-color: rgba(21, 24, 33, 0.18);
26
+ --button-hover-color: var(--tmc-color-surface-muted, #e7ebf0);
27
+ --button-focus-color: var(--tmc-color-focus, #d99a00);
28
+ align-items: center;
29
+ background-color: var(--button-color);
30
+ border: none;
31
+ border-radius: 15px;
32
+ box-shadow:
33
+ 3px 3px 6px 1px var(--button-shadow-color),
34
+ inset -1px -1px 20px -8px var(--button-shadow-color);
35
+ color: var(--button-text-color);
36
+ cursor: pointer;
37
+ display: inline-flex;
38
+ font-size: ${({ $size }) => sizeStyles[$size].fontSize};
39
+ font-weight: 600;
40
+ gap: 0.5rem;
41
+ height: auto;
42
+ justify-content: center;
43
+ margin: 10px;
44
+ min-height: ${({ $size }) => sizeStyles[$size].minHeight};
45
+ padding: ${({ $size }) => sizeStyles[$size].padding};
46
+ transition:
47
+ background-color 0.2s ease,
48
+ box-shadow 0.2s ease,
49
+ transform 0.2s ease;
50
+ width: 100%;
51
+
52
+ &:hover {
53
+ background-color: var(--button-hover-color);
54
+ }
55
+
56
+ &:active {
57
+ transform: translateY(1px);
58
+ }
59
+
60
+ &:focus-visible {
61
+ outline: 2px solid var(--button-focus-color);
62
+ outline-offset: 3px;
63
+ }
64
+
65
+ &:disabled,
66
+ &[aria-disabled="true"] {
67
+ cursor: not-allowed;
68
+ opacity: 0.55;
69
+ pointer-events: none;
70
+ }
71
+ `;
72
+ function resolveButtonPalette(styleType) {
73
+ switch (styleType) {
74
+ case "submit":
75
+ return getDynamicPalette("#2454D6");
76
+ case "success":
77
+ return getDynamicPalette("#1F9D55");
78
+ case "warning":
79
+ return getDynamicPalette("#D97706");
80
+ case "danger":
81
+ return getDynamicPalette("#DC2626");
82
+ case "normal":
83
+ default:
84
+ return {
85
+ background: "var(--tmc-button-surface, var(--tmc-color-surface-raised, #fbfcfe))",
86
+ focus: "var(--tmc-button-focus, var(--tmc-color-focus, #d99a00))",
87
+ hover: "var(--tmc-button-hover, var(--tmc-color-surface-muted, #e7ebf0))",
88
+ shadow: "var(--tmc-button-shadow, rgba(21, 24, 33, 0.18))",
89
+ text: "var(--tmc-button-text, var(--tmc-color-text, #151821))",
90
+ };
91
+ }
92
+ }
93
+ function getDynamicPalette(background) {
94
+ return {
95
+ background,
96
+ focus: darken(background, 28),
97
+ hover: darken(background, 24),
98
+ shadow: darken(background, 54),
99
+ text: getReadableTextColor(background),
100
+ };
101
+ }
102
+ function isButtonActive(active) {
103
+ return active !== false && active !== "false";
104
+ }
105
+ export function Button({ active = true, btype = "button", className, labeling, onClick, onMouseOver, size = "md", tone = "neutral", buttonStyleType = toneToStyleType[tone], style, type, children, ...props }) {
106
+ const palette = resolveButtonPalette(buttonStyleType);
107
+ const cssVariables = {
108
+ "--button-color": palette.background,
109
+ "--button-focus-color": palette.focus,
110
+ "--button-hover-color": palette.hover,
111
+ "--button-shadow-color": palette.shadow,
112
+ "--button-text-color": palette.text,
113
+ };
114
+ return (_jsx(StyledButton, { "$size": size, className: className, onClick: (event) => {
115
+ if (!isButtonActive(active)) {
116
+ event.preventDefault();
117
+ return;
118
+ }
119
+ onClick?.(event);
120
+ }, onMouseOver: (event) => {
121
+ onMouseOver?.(event);
122
+ }, style: { ...cssVariables, ...style }, type: buttonStyleType === "submit" ? "submit" : (type ?? btype), ...props, children: children ?? labeling }));
21
123
  }
@@ -1,8 +1,13 @@
1
1
  import type { ButtonHTMLAttributes, ReactNode } from "react";
2
2
  export type ButtonSize = "sm" | "md";
3
3
  export type ButtonTone = "neutral" | "accent" | "danger";
4
+ export type LegacyButtonStyleType = "danger" | "normal" | "submit" | "success" | "warning";
4
5
  export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
5
- children: ReactNode;
6
+ active?: boolean | "false" | "true";
7
+ btype?: ButtonHTMLAttributes<HTMLButtonElement>["type"];
8
+ buttonStyleType?: LegacyButtonStyleType;
9
+ children?: ReactNode;
10
+ labeling?: ReactNode;
6
11
  size?: ButtonSize;
7
12
  tone?: ButtonTone;
8
13
  }
@@ -1,2 +1,2 @@
1
1
  export { Button } from "./Button";
2
- export type { ButtonProps, ButtonSize, ButtonTone } from "./Button.types";
2
+ export type { ButtonProps, ButtonSize, ButtonTone, LegacyButtonStyleType } from "./Button.types";
@@ -0,0 +1,2 @@
1
+ import type { CheckboxProps } from "./Checkbox.types";
2
+ export declare function Checkbox({ description, label, children, ...props }: CheckboxProps): import("react").JSX.Element;
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import styled from "@emotion/styled";
4
+ import { useContrastedFieldStyle } from "../text-input/shared";
5
+ const CheckboxRoot = styled.label `
6
+ align-items: flex-start;
7
+ color: var(--tmc-field-label-text, var(--tmc-field-auto-text, var(--tmc-color-text, #151821)));
8
+ cursor: pointer;
9
+ display: inline-flex;
10
+ gap: 0.7rem;
11
+ `;
12
+ const CheckboxInput = styled.input `
13
+ accent-color: var(--cta-color, #30fa00);
14
+ cursor: pointer;
15
+ flex: 0 0 auto;
16
+ height: 1.1rem;
17
+ margin-top: 0.1rem;
18
+ width: 1.1rem;
19
+
20
+ &:focus-visible {
21
+ outline: 2px solid var(--cta-color, #30fa00);
22
+ outline-offset: 3px;
23
+ }
24
+
25
+ &:disabled {
26
+ cursor: not-allowed;
27
+ opacity: 0.55;
28
+ }
29
+ `;
30
+ const CheckboxCopy = styled.span `
31
+ display: grid;
32
+ gap: 0.2rem;
33
+ line-height: 1.35;
34
+ `;
35
+ const CheckboxLabel = styled.span `
36
+ color: inherit;
37
+ font-size: 0.95rem;
38
+ font-weight: 600;
39
+ `;
40
+ const CheckboxDescription = styled.span `
41
+ color: var(--tmc-field-description-text, var(--tmc-color-text-muted, var(--tmc-field-auto-text, #5f6b7a)));
42
+ font-size: 0.82rem;
43
+ `;
44
+ export function Checkbox({ description, label, children, ...props }) {
45
+ const fieldStyle = useContrastedFieldStyle();
46
+ return (_jsxs(CheckboxRoot, { ...fieldStyle, children: [_jsx(CheckboxInput, { type: "checkbox", ...props }), (label || description || children) && (_jsxs(CheckboxCopy, { children: [(label || children) && _jsx(CheckboxLabel, { children: label ?? children }), description && _jsx(CheckboxDescription, { children: description })] }))] }));
47
+ }
@@ -0,0 +1,5 @@
1
+ import type { InputHTMLAttributes, ReactNode } from "react";
2
+ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
3
+ description?: ReactNode;
4
+ label?: ReactNode;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { CheckboxProps } from "./Checkbox.types";
2
+ export declare function Switch({ description, label, children, ...props }: CheckboxProps): import("react").JSX.Element;
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import styled from "@emotion/styled";
4
+ import { useContrastedFieldStyle } from "../text-input/shared";
5
+ const SwitchRoot = styled.label `
6
+ align-items: center;
7
+ color: var(--tmc-field-label-text, var(--tmc-field-auto-text, var(--tmc-color-text, #151821)));
8
+ cursor: pointer;
9
+ display: inline-flex;
10
+ gap: 0.75rem;
11
+ `;
12
+ const SwitchInput = styled.input `
13
+ height: 0;
14
+ opacity: 0;
15
+ position: absolute;
16
+ width: 0;
17
+ `;
18
+ const SwitchTrack = styled.span `
19
+ background-color: color-mix(in srgb, var(--tmc-color-surface-muted, #e7ebf0) 78%, var(--tmc-color-text, #151821) 22%);
20
+ border: 1px solid color-mix(in srgb, var(--tmc-color-text-muted, #5f6b7a) 38%, transparent);
21
+ border-radius: 34px;
22
+ display: inline-block;
23
+ flex: 0 0 auto;
24
+ height: 34px;
25
+ position: relative;
26
+ transition: background-color 0.4s ease, box-shadow 0.2s ease;
27
+ width: 60px;
28
+
29
+ &::before {
30
+ background-color: var(--tmc-switch-thumb, #f8fafc);
31
+ border-radius: 50%;
32
+ bottom: 4px;
33
+ box-shadow: 0 1px 4px rgb(0 0 0 / 0.3), 0 0 0 1px rgb(255 255 255 / 0.18);
34
+ content: "";
35
+ height: 24px;
36
+ left: 4px;
37
+ position: absolute;
38
+ transition: transform 0.4s ease;
39
+ width: 24px;
40
+ }
41
+ `;
42
+ const SwitchCopy = styled.span `
43
+ display: grid;
44
+ gap: 0.2rem;
45
+ line-height: 1.35;
46
+ `;
47
+ const SwitchLabel = styled.span `
48
+ color: inherit;
49
+ font-size: 0.95rem;
50
+ font-weight: 600;
51
+ `;
52
+ const SwitchDescription = styled.span `
53
+ color: var(--tmc-field-description-text, var(--tmc-color-text-muted, var(--tmc-field-auto-text, #5f6b7a)));
54
+ font-size: 0.82rem;
55
+ `;
56
+ const SwitchControl = styled.span `
57
+ display: inline-flex;
58
+
59
+ .tmc-switch-input:checked + .tmc-switch-track {
60
+ background-color: var(--cta-color, var(--tmc-color-accent, #2454d6));
61
+ }
62
+
63
+ .tmc-switch-input:focus-visible + .tmc-switch-track {
64
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--cta-color, var(--tmc-color-accent, #2454d6)) 35%, transparent);
65
+ }
66
+
67
+ .tmc-switch-input:checked + .tmc-switch-track::before {
68
+ transform: translateX(28px);
69
+ }
70
+
71
+ .tmc-switch-input:disabled + .tmc-switch-track {
72
+ cursor: not-allowed;
73
+ opacity: 0.55;
74
+ }
75
+ `;
76
+ export function Switch({ description, label, children, ...props }) {
77
+ const fieldStyle = useContrastedFieldStyle();
78
+ return (_jsxs(SwitchRoot, { ...fieldStyle, children: [_jsxs(SwitchControl, { children: [_jsx(SwitchInput, { className: "tmc-switch-input", type: "checkbox", ...props }), _jsx(SwitchTrack, { className: "tmc-switch-track" })] }), (label || description || children) && (_jsxs(SwitchCopy, { children: [(label || children) && _jsx(SwitchLabel, { children: label ?? children }), description && _jsx(SwitchDescription, { children: description })] }))] }));
79
+ }
@@ -0,0 +1,3 @@
1
+ export { Checkbox } from "./Checkbox";
2
+ export { Switch } from "./Switch";
3
+ export type { CheckboxProps } from "./Checkbox.types";
@@ -0,0 +1,2 @@
1
+ export { Checkbox } from "./Checkbox";
2
+ export { Switch } from "./Switch";
@@ -0,0 +1,2 @@
1
+ import type { CodeBlockProps } from "./CodeBlock.types";
2
+ export declare function CodeBlock({ code, filename, language, ...props }: CodeBlockProps): import("react").JSX.Element;
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import styled from "@emotion/styled";
4
+ import { useState } from "react";
5
+ const CodeBlockRoot = styled.div `
6
+ background: #111722;
7
+ border: 1px solid #273244;
8
+ border-radius: 8px;
9
+ color: #eef4ff;
10
+ overflow: hidden;
11
+ `;
12
+ const CodeBlockHeader = styled.div `
13
+ align-items: center;
14
+ background: #182132;
15
+ border-bottom: 1px solid #273244;
16
+ display: flex;
17
+ gap: 0.75rem;
18
+ justify-content: space-between;
19
+ min-height: 2.35rem;
20
+ padding: 0.45rem 0.75rem;
21
+ `;
22
+ const CodeBlockTitle = styled.span `
23
+ color: #aebbd0;
24
+ font-family: var(--tmc-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
25
+ font-size: 0.78rem;
26
+ `;
27
+ const CopyButton = styled.button `
28
+ background: #24314a;
29
+ border: 1px solid #354561;
30
+ border-radius: 6px;
31
+ color: #eef4ff;
32
+ cursor: pointer;
33
+ font-size: 0.75rem;
34
+ padding: 0.25rem 0.55rem;
35
+
36
+ &:hover {
37
+ background: #2d3b58;
38
+ }
39
+ `;
40
+ const CodePre = styled.pre `
41
+ font-family: var(--tmc-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
42
+ font-size: 0.82rem;
43
+ line-height: 1.65;
44
+ margin: 0;
45
+ overflow-x: auto;
46
+ padding: 1rem;
47
+ `;
48
+ export function CodeBlock({ code, filename, language, ...props }) {
49
+ const [copied, setCopied] = useState(false);
50
+ async function copyCode() {
51
+ await navigator.clipboard.writeText(code);
52
+ setCopied(true);
53
+ window.setTimeout(() => setCopied(false), 1200);
54
+ }
55
+ return (_jsxs(CodeBlockRoot, { ...props, children: [_jsxs(CodeBlockHeader, { children: [_jsx(CodeBlockTitle, { children: filename ?? language ?? "code" }), _jsx(CopyButton, { onClick: () => void copyCode(), type: "button", children: copied ? "Copied" : "Copy" })] }), _jsx(CodePre, { children: _jsx("code", { children: code }) })] }));
56
+ }
@@ -0,0 +1,6 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ export interface CodeBlockProps extends HTMLAttributes<HTMLDivElement> {
3
+ code: string;
4
+ filename?: ReactNode;
5
+ language?: string;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { CodeBlock } from "./CodeBlock";
2
+ export type { CodeBlockProps } from "./CodeBlock.types";
@@ -0,0 +1 @@
1
+ export { CodeBlock } from "./CodeBlock";
@@ -0,0 +1,2 @@
1
+ import type { DividerProps } from "./Divider.types";
2
+ export declare function Divider({ children, orientation, role, ...props }: DividerProps): import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import styled from "@emotion/styled";
4
+ const DividerRoot = styled.div `
5
+ align-items: center;
6
+ color: var(--tmc-color-text-muted, #5f6b7a);
7
+ display: flex;
8
+ font-size: 0.78rem;
9
+ font-weight: 700;
10
+ gap: 0.75rem;
11
+ letter-spacing: 0.08em;
12
+ text-transform: uppercase;
13
+ width: ${({ $orientation }) => $orientation === "horizontal" ? "100%" : "auto"};
14
+
15
+ &::before,
16
+ &::after {
17
+ background: var(--tmc-color-border, #d7dde5);
18
+ content: "";
19
+ display: block;
20
+ flex: 1 1 auto;
21
+ height: ${({ $orientation }) => $orientation === "horizontal" ? "1px" : "100%"};
22
+ min-height: ${({ $orientation }) => $orientation === "vertical" ? "1.5rem" : "0"};
23
+ width: ${({ $orientation }) => $orientation === "vertical" ? "1px" : "auto"};
24
+ }
25
+
26
+ &:empty {
27
+ gap: 0;
28
+ }
29
+
30
+ &:empty::before {
31
+ display: none;
32
+ }
33
+ `;
34
+ export function Divider({ children, orientation = "horizontal", role, ...props }) {
35
+ return (_jsx(DividerRoot, { "$orientation": orientation, role: role ?? "separator", ...props, children: children }));
36
+ }
@@ -0,0 +1,5 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+ export interface DividerProps extends HTMLAttributes<HTMLDivElement> {
3
+ children?: ReactNode;
4
+ orientation?: "horizontal" | "vertical";
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { Divider } from "./Divider";
2
+ export type { DividerProps } from "./Divider.types";
@@ -0,0 +1 @@
1
+ export { Divider } from "./Divider";
@@ -0,0 +1,3 @@
1
+ import type { RadioGroupProps, RadioProps } from "./Radio.types";
2
+ export declare function Radio({ description, label, children, ...props }: RadioProps): import("react").JSX.Element;
3
+ export declare function RadioGroup({ defaultValue, description, label, name, onChange, onValueChange, options, value, ...props }: RadioGroupProps): import("react").JSX.Element;
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import styled from "@emotion/styled";
4
+ import { useContrastedFieldStyle } from "../text-input/shared";
5
+ const RadioRoot = styled.label `
6
+ align-items: flex-start;
7
+ color: var(--tmc-field-label-text, var(--tmc-field-auto-text, var(--tmc-color-text, #151821)));
8
+ cursor: pointer;
9
+ display: inline-flex;
10
+ gap: 0.7rem;
11
+ `;
12
+ const RadioInput = styled.input `
13
+ accent-color: var(--cta-color, var(--tmc-color-accent, #2454d6));
14
+ cursor: pointer;
15
+ flex: 0 0 auto;
16
+ height: 1.1rem;
17
+ margin-top: 0.1rem;
18
+ width: 1.1rem;
19
+
20
+ &:focus-visible {
21
+ outline: 2px solid var(--tmc-color-accent, #2454d6);
22
+ outline-offset: 3px;
23
+ }
24
+
25
+ &:disabled {
26
+ cursor: not-allowed;
27
+ opacity: 0.55;
28
+ }
29
+ `;
30
+ const Copy = styled.span `
31
+ display: grid;
32
+ gap: 0.2rem;
33
+ line-height: 1.35;
34
+ `;
35
+ const Label = styled.span `
36
+ color: inherit;
37
+ font-size: 0.95rem;
38
+ font-weight: 600;
39
+ `;
40
+ const Description = styled.span `
41
+ color: var(--tmc-field-description-text, var(--tmc-color-text-muted, var(--tmc-field-auto-text, #5f6b7a)));
42
+ font-size: 0.82rem;
43
+ `;
44
+ const GroupRoot = styled.fieldset `
45
+ border: 0;
46
+ color: var(--tmc-field-label-text, var(--tmc-field-auto-text, var(--tmc-color-text, #151821)));
47
+ display: grid;
48
+ gap: 0.65rem;
49
+ margin: 0;
50
+ min-inline-size: 0;
51
+ padding: 0;
52
+ `;
53
+ const GroupLegend = styled.legend `
54
+ color: inherit;
55
+ font-size: 0.9rem;
56
+ font-weight: 650;
57
+ margin: 0 0 0.35rem;
58
+ padding: 0;
59
+ `;
60
+ const GroupDescription = styled.p `
61
+ color: var(--tmc-field-description-text, var(--tmc-color-text-muted, var(--tmc-field-auto-text, #5f6b7a)));
62
+ font-size: 0.82rem;
63
+ line-height: 1.45;
64
+ margin: -0.25rem 0 0.25rem;
65
+ `;
66
+ const Options = styled.div `
67
+ display: grid;
68
+ gap: 0.75rem;
69
+ `;
70
+ export function Radio({ description, label, children, ...props }) {
71
+ const fieldStyle = useContrastedFieldStyle();
72
+ return (_jsxs(RadioRoot, { ...fieldStyle, children: [_jsx(RadioInput, { type: "radio", ...props }), (label || description || children) && (_jsxs(Copy, { children: [(label || children) && _jsx(Label, { children: label ?? children }), description && _jsx(Description, { children: description })] }))] }));
73
+ }
74
+ export function RadioGroup({ defaultValue, description, label, name, onChange, onValueChange, options, value, ...props }) {
75
+ const fieldStyle = useContrastedFieldStyle();
76
+ return (_jsxs(GroupRoot, { ...fieldStyle, children: [label ? _jsx(GroupLegend, { children: label }) : null, description ? _jsx(GroupDescription, { children: description }) : null, _jsx(Options, { children: options.map((option) => (_jsx(Radio, { checked: value !== undefined ? value === option.value : undefined, defaultChecked: defaultValue === option.value, description: option.description, disabled: props.disabled || option.disabled, name: name, onChange: (event) => { onChange?.(event); if (event.target.checked) {
77
+ onValueChange?.(option.value);
78
+ } }, required: props.required, value: option.value, children: option.label }, option.value))) })] }));
79
+ }
@@ -0,0 +1,20 @@
1
+ import type { ChangeEvent, InputHTMLAttributes, ReactNode } from "react";
2
+ export interface RadioOption {
3
+ description?: ReactNode;
4
+ disabled?: boolean;
5
+ label: ReactNode;
6
+ value: string;
7
+ }
8
+ export interface RadioProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
9
+ description?: ReactNode;
10
+ label?: ReactNode;
11
+ }
12
+ export interface RadioGroupProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "children" | "defaultValue" | "onChange" | "type" | "value"> {
13
+ defaultValue?: string;
14
+ description?: ReactNode;
15
+ label?: ReactNode;
16
+ onChange?: (event: ChangeEvent<HTMLInputElement>) => void;
17
+ onValueChange?: (value: string) => void;
18
+ options: RadioOption[];
19
+ value?: string;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { Radio, RadioGroup } from "./Radio";
2
+ export type { RadioGroupProps, RadioOption, RadioProps } from "./Radio.types";
@@ -0,0 +1 @@
1
+ export { Radio, RadioGroup } from "./Radio";
@@ -0,0 +1,2 @@
1
+ import type { SliderProps } from "./Slider.types";
2
+ export declare function Slider({ label, showValue, value, valueLabel, defaultValue, ...props }: SliderProps): import("react").JSX.Element;
@@ -0,0 +1,86 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import styled from "@emotion/styled";
4
+ import { useContrastedFieldStyle } from "../text-input/shared";
5
+ const SliderRoot = styled.label `
6
+ color: var(--tmc-field-label-text, var(--tmc-field-auto-text, var(--primary-text, var(--tmc-color-text, #151821))));
7
+ display: grid;
8
+ gap: 0.55rem;
9
+ width: 100%;
10
+ `;
11
+ const SliderHeader = styled.span `
12
+ align-items: center;
13
+ display: flex;
14
+ font-size: 0.9rem;
15
+ font-weight: 600;
16
+ gap: 0.75rem;
17
+ justify-content: space-between;
18
+ `;
19
+ const SliderValue = styled.span `
20
+ color: var(--tmc-field-description-text, var(--tmc-field-auto-text, var(--secondary-text, var(--tmc-color-text-muted, #5f6b7a))));
21
+ font-size: 0.8rem;
22
+ font-variant-numeric: tabular-nums;
23
+ `;
24
+ const SliderInput = styled.input `
25
+ --slider-track: var(--neutral-600, var(--tmc-color-surface-muted, #e7ebf0));
26
+ --slider-fill: var(--cta-color, var(--tmc-color-accent, #2454d6));
27
+ --slider-thumb: var(--tmc-color-surface-raised, #fbfcfe);
28
+ appearance: none;
29
+ background: transparent;
30
+ cursor: pointer;
31
+ height: 1.5rem;
32
+ width: 100%;
33
+
34
+ &::-webkit-slider-runnable-track {
35
+ background: linear-gradient(90deg, var(--slider-fill), var(--slider-track));
36
+ border-radius: 999px;
37
+ height: 0.45rem;
38
+ }
39
+
40
+ &::-webkit-slider-thumb {
41
+ appearance: none;
42
+ background: var(--slider-thumb);
43
+ border: 2px solid var(--slider-fill);
44
+ border-radius: 999px;
45
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.24);
46
+ height: 1.15rem;
47
+ margin-top: -0.35rem;
48
+ width: 1.15rem;
49
+ }
50
+
51
+ &::-moz-range-track {
52
+ background: var(--slider-track);
53
+ border-radius: 999px;
54
+ height: 0.45rem;
55
+ }
56
+
57
+ &::-moz-range-progress {
58
+ background: var(--slider-fill);
59
+ border-radius: 999px;
60
+ height: 0.45rem;
61
+ }
62
+
63
+ &::-moz-range-thumb {
64
+ background: var(--slider-thumb);
65
+ border: 2px solid var(--slider-fill);
66
+ border-radius: 999px;
67
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.24);
68
+ height: 1.15rem;
69
+ width: 1.15rem;
70
+ }
71
+
72
+ &:focus-visible {
73
+ outline: 2px solid var(--slider-fill);
74
+ outline-offset: 4px;
75
+ }
76
+
77
+ &:disabled {
78
+ cursor: not-allowed;
79
+ opacity: 0.55;
80
+ }
81
+ `;
82
+ export function Slider({ label, showValue, value, valueLabel, defaultValue, ...props }) {
83
+ const renderedValue = valueLabel ?? value ?? defaultValue;
84
+ const fieldStyle = useContrastedFieldStyle();
85
+ return (_jsxs(SliderRoot, { ...fieldStyle, children: [(label || showValue) && (_jsxs(SliderHeader, { children: [label && _jsx("span", { children: label }), showValue && renderedValue !== undefined && _jsx(SliderValue, { children: renderedValue })] })), _jsx(SliderInput, { defaultValue: defaultValue, type: "range", value: value, ...props })] }));
86
+ }
@@ -0,0 +1,6 @@
1
+ import type { InputHTMLAttributes, ReactNode } from "react";
2
+ export interface SliderProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type"> {
3
+ label?: ReactNode;
4
+ showValue?: boolean;
5
+ valueLabel?: ReactNode;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { Slider } from "./Slider";
2
+ export type { SliderProps } from "./Slider.types";
@@ -0,0 +1 @@
1
+ export { Slider } from "./Slider";
@@ -0,0 +1,6 @@
1
+ import type { TextInputProps } from "./TextInput.types";
2
+ export type AuthFieldProps = TextInputProps;
3
+ export declare function UsernameField({ autoComplete, label, type, ...props }: AuthFieldProps): import("react").JSX.Element;
4
+ export declare function EmailField({ autoComplete, inputMode, label, type, ...props }: AuthFieldProps): import("react").JSX.Element;
5
+ export declare function PasswordField({ autoComplete, label, type, ...props }: AuthFieldProps): import("react").JSX.Element;
6
+ export declare function PhoneNumberField({ autoComplete, inputMode, label, type, ...props }: AuthFieldProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { TextInput } from "./TextInput";
4
+ export function UsernameField({ autoComplete = "username", label = "Username", type, ...props }) {
5
+ return _jsx(TextInput, { autoComplete: autoComplete, label: label, type: type ?? "text", ...props });
6
+ }
7
+ export function EmailField({ autoComplete = "email", inputMode = "email", label = "Email", type, ...props }) {
8
+ return _jsx(TextInput, { autoComplete: autoComplete, inputMode: inputMode, label: label, type: type ?? "email", ...props });
9
+ }
10
+ export function PasswordField({ autoComplete = "current-password", label = "Password", type, ...props }) {
11
+ return _jsx(TextInput, { autoComplete: autoComplete, label: label, type: type ?? "password", ...props });
12
+ }
13
+ export function PhoneNumberField({ autoComplete = "tel", inputMode = "tel", label = "Phone number", type, ...props }) {
14
+ return _jsx(TextInput, { autoComplete: autoComplete, inputMode: inputMode, label: label, type: type ?? "tel", ...props });
15
+ }
@@ -0,0 +1,2 @@
1
+ import type { TextAreaProps } from "./TextInput.types";
2
+ export declare function TextArea({ description, error, label, "aria-invalid": ariaInvalid, style, ...props }: TextAreaProps): import("react").JSX.Element;
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import styled from "@emotion/styled";
4
+ import { FieldDescription, FieldError, FieldLabel, FieldRoot, inputSurfaceStyles, useContrastedFieldStyle, useContrastedInputStyle, } from "./shared";
5
+ const StyledTextArea = styled.textarea `
6
+ ${inputSurfaceStyles}
7
+ line-height: 1.5;
8
+ max-height: var(--tmc-textarea-max-height, 18rem);
9
+ min-height: 7rem;
10
+ overflow: auto;
11
+ padding: 0.75rem 0.85rem;
12
+ resize: vertical;
13
+ `;
14
+ export function TextArea({ description, error, label, "aria-invalid": ariaInvalid, style, ...props }) {
15
+ const fieldStyle = useContrastedFieldStyle();
16
+ const inputStyle = useContrastedInputStyle(style);
17
+ return (_jsxs(FieldRoot, { ...fieldStyle, children: [label && _jsx(FieldLabel, { children: label }), _jsx(StyledTextArea, { "aria-invalid": ariaInvalid ?? Boolean(error), ...inputStyle, ...props }), description && !error ? _jsx(FieldDescription, { children: description }) : null, error ? _jsx(FieldError, { children: error }) : null] }));
18
+ }
@@ -0,0 +1,2 @@
1
+ import type { TextInputProps } from "./TextInput.types";
2
+ export declare function TextInput({ description, error, label, "aria-invalid": ariaInvalid, style, ...props }: TextInputProps): import("react").JSX.Element;
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import styled from "@emotion/styled";
4
+ import { FieldDescription, FieldError, FieldLabel, FieldRoot, inputSurfaceStyles, useContrastedFieldStyle, useContrastedInputStyle, } from "./shared";
5
+ const StyledInput = styled.input `
6
+ ${inputSurfaceStyles}
7
+ min-height: 2.6rem;
8
+ padding: 0 0.85rem;
9
+ `;
10
+ export function TextInput({ description, error, label, "aria-invalid": ariaInvalid, style, ...props }) {
11
+ const fieldStyle = useContrastedFieldStyle();
12
+ const inputStyle = useContrastedInputStyle(style);
13
+ return (_jsxs(FieldRoot, { ...fieldStyle, children: [label && _jsx(FieldLabel, { children: label }), _jsx(StyledInput, { "aria-invalid": ariaInvalid ?? Boolean(error), ...inputStyle, ...props }), description && !error ? _jsx(FieldDescription, { children: description }) : null, error ? _jsx(FieldError, { children: error }) : null] }));
14
+ }
@@ -0,0 +1,11 @@
1
+ import type { InputHTMLAttributes, ReactNode, TextareaHTMLAttributes } from "react";
2
+ export interface TextInputProps extends InputHTMLAttributes<HTMLInputElement> {
3
+ description?: ReactNode;
4
+ error?: ReactNode;
5
+ label?: ReactNode;
6
+ }
7
+ export interface TextAreaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
8
+ description?: ReactNode;
9
+ error?: ReactNode;
10
+ label?: ReactNode;
11
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export { EmailField, PasswordField, PhoneNumberField, UsernameField } from "./AuthFields";
2
+ export { TextArea } from "./TextArea";
3
+ export { TextInput } from "./TextInput";
4
+ export type { AuthFieldProps } from "./AuthFields";
5
+ export type { TextAreaProps, TextInputProps } from "./TextInput.types";
@@ -0,0 +1,3 @@
1
+ export { EmailField, PasswordField, PhoneNumberField, UsernameField } from "./AuthFields";
2
+ export { TextArea } from "./TextArea";
3
+ export { TextInput } from "./TextInput";
@@ -0,0 +1,26 @@
1
+ import type { CSSProperties } from "react";
2
+ export declare const FieldRoot: import("@emotion/styled").StyledComponent<{
3
+ theme?: import("@emotion/react").Theme;
4
+ as?: React.ElementType;
5
+ }, import("react").DetailedHTMLProps<import("react").LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>, {}>;
6
+ export declare const FieldLabel: import("@emotion/styled").StyledComponent<{
7
+ theme?: import("@emotion/react").Theme;
8
+ as?: React.ElementType;
9
+ }, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, {}>;
10
+ export declare const FieldDescription: import("@emotion/styled").StyledComponent<{
11
+ theme?: import("@emotion/react").Theme;
12
+ as?: React.ElementType;
13
+ }, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, {}>;
14
+ export declare const FieldError: import("@emotion/styled").StyledComponent<{
15
+ theme?: import("@emotion/react").Theme;
16
+ as?: React.ElementType;
17
+ }, import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, {}>;
18
+ export declare const inputSurfaceStyles = "\n background: var(--tmc-color-surface-raised, #fbfcfe);\n border: 1px solid var(--tmc-color-border, #d7dde5);\n border-radius: var(--tmc-radius-md, 6px);\n color: var(--tmc-input-text, var(--tmc-input-auto-text, var(--tmc-color-on-surface-raised, #151821)));\n font: inherit;\n outline: none;\n transition:\n background-color 0.18s ease,\n border-color 0.18s ease,\n box-shadow 0.18s ease;\n width: 100%;\n\n &::placeholder {\n color: var(--tmc-input-placeholder-text, var(--tmc-input-auto-text, var(--tmc-color-text-muted, #5f6b7a)));\n opacity: 0.72;\n }\n\n &:focus {\n border-color: var(--tmc-color-accent, #2454d6);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--tmc-color-accent, #2454d6) 24%, transparent);\n }\n\n &:disabled {\n cursor: not-allowed;\n opacity: 0.58;\n }\n\n &[aria-invalid=\"true\"] {\n border-color: var(--tmc-color-danger, #b42318);\n }\n";
19
+ export declare function useContrastedFieldStyle<T extends HTMLElement>(style?: CSSProperties): {
20
+ ref: import("react").RefObject<T | null>;
21
+ style: CSSProperties;
22
+ };
23
+ export declare function useContrastedInputStyle<T extends HTMLElement>(style?: CSSProperties): {
24
+ ref: import("react").RefObject<T | null>;
25
+ style: CSSProperties;
26
+ };
@@ -0,0 +1,113 @@
1
+ import styled from "@emotion/styled";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { getReadableTextColor, rgbToHex } from "../../utils/color";
4
+ export const FieldRoot = styled.label `
5
+ color: var(--tmc-field-label-text, var(--tmc-field-auto-text, var(--tmc-color-text, #151821)));
6
+ display: grid;
7
+ gap: 0.45rem;
8
+ width: 100%;
9
+ `;
10
+ export const FieldLabel = styled.span `
11
+ color: inherit;
12
+ font-size: 0.9rem;
13
+ font-weight: 650;
14
+ `;
15
+ export const FieldDescription = styled.span `
16
+ color: var(--tmc-field-description-text, var(--tmc-color-text-muted, var(--tmc-field-auto-text, #5f6b7a)));
17
+ font-size: 0.82rem;
18
+ line-height: 1.45;
19
+ `;
20
+ export const FieldError = styled.span `
21
+ color: var(--tmc-color-danger, #b42318);
22
+ font-size: 0.82rem;
23
+ line-height: 1.45;
24
+ `;
25
+ export const inputSurfaceStyles = `
26
+ background: var(--tmc-color-surface-raised, #fbfcfe);
27
+ border: 1px solid var(--tmc-color-border, #d7dde5);
28
+ border-radius: var(--tmc-radius-md, 6px);
29
+ color: var(--tmc-input-text, var(--tmc-input-auto-text, var(--tmc-color-on-surface-raised, #151821)));
30
+ font: inherit;
31
+ outline: none;
32
+ transition:
33
+ background-color 0.18s ease,
34
+ border-color 0.18s ease,
35
+ box-shadow 0.18s ease;
36
+ width: 100%;
37
+
38
+ &::placeholder {
39
+ color: var(--tmc-input-placeholder-text, var(--tmc-input-auto-text, var(--tmc-color-text-muted, #5f6b7a)));
40
+ opacity: 0.72;
41
+ }
42
+
43
+ &:focus {
44
+ border-color: var(--tmc-color-accent, #2454d6);
45
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--tmc-color-accent, #2454d6) 24%, transparent);
46
+ }
47
+
48
+ &:disabled {
49
+ cursor: not-allowed;
50
+ opacity: 0.58;
51
+ }
52
+
53
+ &[aria-invalid="true"] {
54
+ border-color: var(--tmc-color-danger, #b42318);
55
+ }
56
+ `;
57
+ function isTransparentBackground(color) {
58
+ return !color || color === "transparent" || /rgba\([^)]*,\s*(?:0|0?\.0+)\s*\)$/i.test(color);
59
+ }
60
+ function getEffectiveBackground(element) {
61
+ let current = element;
62
+ while (current) {
63
+ const background = window.getComputedStyle(current).backgroundColor;
64
+ if (!isTransparentBackground(background)) {
65
+ return background;
66
+ }
67
+ current = current.parentElement;
68
+ }
69
+ const bodyBackground = window.getComputedStyle(document.body).backgroundColor;
70
+ return isTransparentBackground(bodyBackground) ? window.getComputedStyle(document.documentElement).backgroundColor : bodyBackground;
71
+ }
72
+ export function useContrastedFieldStyle(style) {
73
+ const ref = useRef(null);
74
+ const [textColor, setTextColor] = useState();
75
+ useEffect(() => {
76
+ const element = ref.current;
77
+ if (!element) {
78
+ return;
79
+ }
80
+ const hex = rgbToHex(getEffectiveBackground(element));
81
+ if (hex) {
82
+ setTextColor(getReadableTextColor(hex));
83
+ }
84
+ }, [style]);
85
+ return {
86
+ ref,
87
+ style: {
88
+ "--tmc-field-auto-text": textColor,
89
+ ...style,
90
+ },
91
+ };
92
+ }
93
+ export function useContrastedInputStyle(style) {
94
+ const ref = useRef(null);
95
+ const [textColor, setTextColor] = useState();
96
+ useEffect(() => {
97
+ const element = ref.current;
98
+ if (!element) {
99
+ return;
100
+ }
101
+ const hex = rgbToHex(getEffectiveBackground(element));
102
+ if (hex) {
103
+ setTextColor(getReadableTextColor(hex));
104
+ }
105
+ }, [style]);
106
+ return {
107
+ ref,
108
+ style: {
109
+ "--tmc-input-auto-text": textColor,
110
+ ...style,
111
+ },
112
+ };
113
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,11 @@
1
1
  export * from "./components/button";
2
+ export * from "./components/code-block";
3
+ export * from "./components/checkbox";
4
+ export * from "./components/divider";
5
+ export * from "./components/radio";
6
+ export * from "./components/slider";
7
+ export * from "./components/text-input";
2
8
  export * from "./config/component-config";
3
9
  export * from "./path-map";
4
10
  export * from "./tokens";
11
+ export * from "./utils/color";
package/dist/index.js CHANGED
@@ -1,4 +1,11 @@
1
1
  export * from "./components/button";
2
+ export * from "./components/code-block";
3
+ export * from "./components/checkbox";
4
+ export * from "./components/divider";
5
+ export * from "./components/radio";
6
+ export * from "./components/slider";
7
+ export * from "./components/text-input";
2
8
  export * from "./config/component-config";
3
9
  export * from "./path-map";
4
10
  export * from "./tokens";
11
+ export * from "./utils/color";
@@ -1,19 +1,67 @@
1
1
  :root {
2
- --tmc-color-surface: #ffffff;
3
- --tmc-color-surface-muted: #f6f7f9;
4
- --tmc-color-surface-raised: #ffffff;
5
- --tmc-color-text: #111827;
2
+ color-scheme: light dark;
3
+ --tmc-color-surface: #f3f5f8;
4
+ --tmc-color-surface-muted: #e7ebf0;
5
+ --tmc-color-surface-raised: #fbfcfe;
6
+ --tmc-color-text: #151821;
6
7
  --tmc-color-text-muted: #5f6b7a;
7
8
  --tmc-color-border: #d7dde5;
8
- --tmc-color-accent: #0b63ce;
9
- --tmc-color-accent-text: #ffffff;
10
- --tmc-color-focus: #ffb020;
9
+ --tmc-color-accent: #2454d6;
10
+ --tmc-color-accent-text: #f7faff;
11
+ --tmc-color-hover: color-mix(in srgb, var(--tmc-color-accent, #2454d6) 10%, transparent);
12
+ --tmc-color-focus: #d99a00;
11
13
  --tmc-color-danger: #b42318;
12
14
  --tmc-radius-sm: 4px;
13
15
  --tmc-radius-md: 6px;
14
16
  --tmc-radius-lg: 8px;
15
17
  }
16
18
 
19
+ @media (prefers-color-scheme: dark) {
20
+ :root {
21
+ --tmc-color-surface: #181b22;
22
+ --tmc-color-surface-muted: #232832;
23
+ --tmc-color-surface-raised: #20242d;
24
+ --tmc-color-text: #f2f5f8;
25
+ --tmc-color-text-muted: #aab4c0;
26
+ --tmc-color-border: #333b48;
27
+ --tmc-color-accent: #6ea3ff;
28
+ --tmc-color-accent-text: #10151f;
29
+ --tmc-color-hover: color-mix(in srgb, var(--tmc-color-accent, #6ea3ff) 14%, transparent);
30
+ --tmc-color-focus: #f2b84b;
31
+ --tmc-color-danger: #ff7a70;
32
+ }
33
+ }
34
+
35
+ :root[data-tmc-theme-mode="light"] {
36
+ color-scheme: light;
37
+ --tmc-color-surface: #f3f5f8;
38
+ --tmc-color-surface-muted: #e7ebf0;
39
+ --tmc-color-surface-raised: #fbfcfe;
40
+ --tmc-color-text: #151821;
41
+ --tmc-color-text-muted: #5f6b7a;
42
+ --tmc-color-border: #d7dde5;
43
+ --tmc-color-accent: #2454d6;
44
+ --tmc-color-accent-text: #f7faff;
45
+ --tmc-color-hover: color-mix(in srgb, var(--tmc-color-accent, #2454d6) 10%, transparent);
46
+ --tmc-color-focus: #d99a00;
47
+ --tmc-color-danger: #b42318;
48
+ }
49
+
50
+ :root[data-tmc-theme-mode="dark"] {
51
+ color-scheme: dark;
52
+ --tmc-color-surface: #181b22;
53
+ --tmc-color-surface-muted: #232832;
54
+ --tmc-color-surface-raised: #20242d;
55
+ --tmc-color-text: #f2f5f8;
56
+ --tmc-color-text-muted: #aab4c0;
57
+ --tmc-color-border: #333b48;
58
+ --tmc-color-accent: #6ea3ff;
59
+ --tmc-color-accent-text: #10151f;
60
+ --tmc-color-hover: color-mix(in srgb, var(--tmc-color-accent, #6ea3ff) 14%, transparent);
61
+ --tmc-color-focus: #f2b84b;
62
+ --tmc-color-danger: #ff7a70;
63
+ }
64
+
17
65
  * {
18
66
  box-sizing: border-box;
19
67
  }
@@ -0,0 +1,6 @@
1
+ export declare function isValidHex(hex: string): boolean;
2
+ export declare function normalizeHex(hex: string): string | null;
3
+ export declare function rgbToHex(rgb: string): string | null;
4
+ export declare function darken(hex: string, amount: number): string;
5
+ export declare function invertAndGrayscaleHex(hex: string): string;
6
+ export declare function getReadableTextColor(hex: string): "#151821" | "#F7FAFC";
@@ -0,0 +1,66 @@
1
+ export function isValidHex(hex) {
2
+ return /^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(hex.trim());
3
+ }
4
+ export function normalizeHex(hex) {
5
+ const value = hex.trim();
6
+ if (/^#[0-9A-Fa-f]{3}$/.test(value)) {
7
+ return `#${value
8
+ .slice(1)
9
+ .split("")
10
+ .map((channel) => `${channel}${channel}`)
11
+ .join("")}`.toUpperCase();
12
+ }
13
+ if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
14
+ return value.toUpperCase();
15
+ }
16
+ return null;
17
+ }
18
+ export function rgbToHex(rgb) {
19
+ const result = rgb.match(/\d+/g);
20
+ if (!result || result.length < 3) {
21
+ return null;
22
+ }
23
+ const [r, g, b] = result.map(Number);
24
+ return `#${[r, g, b]
25
+ .map((channel) => Math.max(0, Math.min(255, channel)).toString(16).padStart(2, "0"))
26
+ .join("")
27
+ .toUpperCase()}`;
28
+ }
29
+ export function darken(hex, amount) {
30
+ const normalized = normalizeHex(hex);
31
+ if (!normalized) {
32
+ return "#000000";
33
+ }
34
+ const r = Math.max(0, parseInt(normalized.slice(1, 3), 16) - amount);
35
+ const g = Math.max(0, parseInt(normalized.slice(3, 5), 16) - amount);
36
+ const b = Math.max(0, parseInt(normalized.slice(5, 7), 16) - amount);
37
+ return `#${[r, g, b]
38
+ .map((channel) => channel.toString(16).padStart(2, "0"))
39
+ .join("")
40
+ .toUpperCase()}`;
41
+ }
42
+ export function invertAndGrayscaleHex(hex) {
43
+ const normalized = normalizeHex(hex);
44
+ if (!normalized) {
45
+ return "#000000";
46
+ }
47
+ const r = 255 - parseInt(normalized.slice(1, 3), 16);
48
+ const g = 255 - parseInt(normalized.slice(3, 5), 16);
49
+ const b = 255 - parseInt(normalized.slice(5, 7), 16);
50
+ const gray = Math.round(0.3 * r + 0.59 * g + 0.11 * b);
51
+ return `#${[gray, gray, gray]
52
+ .map((channel) => channel.toString(16).padStart(2, "0"))
53
+ .join("")
54
+ .toUpperCase()}`;
55
+ }
56
+ export function getReadableTextColor(hex) {
57
+ const normalized = normalizeHex(hex);
58
+ if (!normalized) {
59
+ return "#151821";
60
+ }
61
+ const r = parseInt(normalized.slice(1, 3), 16);
62
+ const g = parseInt(normalized.slice(3, 5), 16);
63
+ const b = parseInt(normalized.slice(5, 7), 16);
64
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000;
65
+ return yiq >= 145 ? "#151821" : "#F7FAFC";
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@themodcraft/core-ui",
3
- "version": "0.0.0",
3
+ "version": "1.1.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -20,6 +20,10 @@
20
20
  "prepack": "npm run build",
21
21
  "typecheck": "tsc -p tsconfig.json --noEmit"
22
22
  },
23
+ "dependencies": {
24
+ "@emotion/react": "^11.14.0",
25
+ "@emotion/styled": "^11.14.1"
26
+ },
23
27
  "peerDependencies": {
24
28
  "react": "^19.0.0",
25
29
  "react-dom": "^19.0.0"