sparkle-design 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +5 -0
  3. package/README.en.md +275 -0
  4. package/README.md +328 -0
  5. package/dist/components/ui/badge/index.d.ts +67 -0
  6. package/dist/components/ui/badge/index.figma.d.ts +1 -0
  7. package/dist/components/ui/badge/index.figma.js +28 -0
  8. package/dist/components/ui/badge/index.js +129 -0
  9. package/dist/components/ui/breadcrumb/index.d.ts +70 -0
  10. package/dist/components/ui/breadcrumb/index.figma.d.ts +1 -0
  11. package/dist/components/ui/breadcrumb/index.figma.js +28 -0
  12. package/dist/components/ui/breadcrumb/index.js +96 -0
  13. package/dist/components/ui/button/index.d.ts +145 -0
  14. package/dist/components/ui/button/index.figma.d.ts +1 -0
  15. package/dist/components/ui/button/index.figma.js +59 -0
  16. package/dist/components/ui/button/index.js +375 -0
  17. package/dist/components/ui/card/index.d.ts +193 -0
  18. package/dist/components/ui/card/index.figma.d.ts +1 -0
  19. package/dist/components/ui/card/index.figma.js +28 -0
  20. package/dist/components/ui/card/index.js +219 -0
  21. package/dist/components/ui/checkbox/index.d.ts +62 -0
  22. package/dist/components/ui/checkbox/index.figma.d.ts +1 -0
  23. package/dist/components/ui/checkbox/index.figma.js +34 -0
  24. package/dist/components/ui/checkbox/index.js +192 -0
  25. package/dist/components/ui/dialog/index.d.ts +106 -0
  26. package/dist/components/ui/dialog/index.figma.d.ts +1 -0
  27. package/dist/components/ui/dialog/index.figma.js +21 -0
  28. package/dist/components/ui/dialog/index.js +157 -0
  29. package/dist/components/ui/divider/index.d.ts +44 -0
  30. package/dist/components/ui/divider/index.figma.d.ts +1 -0
  31. package/dist/components/ui/divider/index.figma.js +39 -0
  32. package/dist/components/ui/divider/index.js +81 -0
  33. package/dist/components/ui/form/index.d.ts +171 -0
  34. package/dist/components/ui/form/index.figma.d.ts +1 -0
  35. package/dist/components/ui/form/index.figma.js +129 -0
  36. package/dist/components/ui/form/index.js +214 -0
  37. package/dist/components/ui/form/label.d.ts +4 -0
  38. package/dist/components/ui/form/label.js +20 -0
  39. package/dist/components/ui/icon/index.d.ts +71 -0
  40. package/dist/components/ui/icon/index.figma.d.ts +1 -0
  41. package/dist/components/ui/icon/index.figma.js +1858 -0
  42. package/dist/components/ui/icon/index.js +57 -0
  43. package/dist/components/ui/icon-button/index.d.ts +77 -0
  44. package/dist/components/ui/icon-button/index.figma.d.ts +1 -0
  45. package/dist/components/ui/icon-button/index.figma.js +43 -0
  46. package/dist/components/ui/icon-button/index.js +341 -0
  47. package/dist/components/ui/inline-message/index.d.ts +83 -0
  48. package/dist/components/ui/inline-message/index.figma.d.ts +1 -0
  49. package/dist/components/ui/inline-message/index.figma.js +26 -0
  50. package/dist/components/ui/inline-message/index.js +154 -0
  51. package/dist/components/ui/input/index.d.ts +118 -0
  52. package/dist/components/ui/input/index.figma.d.ts +1 -0
  53. package/dist/components/ui/input/index.figma.js +36 -0
  54. package/dist/components/ui/input/index.js +256 -0
  55. package/dist/components/ui/input-password/index.d.ts +27 -0
  56. package/dist/components/ui/input-password/index.figma.d.ts +1 -0
  57. package/dist/components/ui/input-password/index.figma.js +36 -0
  58. package/dist/components/ui/input-password/index.js +35 -0
  59. package/dist/components/ui/link/index.d.ts +68 -0
  60. package/dist/components/ui/link/index.figma.d.ts +1 -0
  61. package/dist/components/ui/link/index.figma.js +22 -0
  62. package/dist/components/ui/link/index.js +92 -0
  63. package/dist/components/ui/modal/index.d.ts +83 -0
  64. package/dist/components/ui/modal/index.figma.d.ts +1 -0
  65. package/dist/components/ui/modal/index.figma.js +61 -0
  66. package/dist/components/ui/modal/index.js +143 -0
  67. package/dist/components/ui/overlay/index.d.ts +19 -0
  68. package/dist/components/ui/overlay/index.figma.d.ts +1 -0
  69. package/dist/components/ui/overlay/index.figma.js +14 -0
  70. package/dist/components/ui/overlay/index.js +39 -0
  71. package/dist/components/ui/radio/index.d.ts +94 -0
  72. package/dist/components/ui/radio/index.figma.d.ts +1 -0
  73. package/dist/components/ui/radio/index.figma.js +63 -0
  74. package/dist/components/ui/radio/index.js +204 -0
  75. package/dist/components/ui/select/index.d.ts +175 -0
  76. package/dist/components/ui/select/index.figma.d.ts +1 -0
  77. package/dist/components/ui/select/index.figma.js +30 -0
  78. package/dist/components/ui/select/index.js +281 -0
  79. package/dist/components/ui/skeleton/index.d.ts +21 -0
  80. package/dist/components/ui/skeleton/index.figma.d.ts +1 -0
  81. package/dist/components/ui/skeleton/index.figma.js +14 -0
  82. package/dist/components/ui/skeleton/index.js +32 -0
  83. package/dist/components/ui/slider/index.d.ts +75 -0
  84. package/dist/components/ui/slider/index.figma.d.ts +1 -0
  85. package/dist/components/ui/slider/index.figma.js +28 -0
  86. package/dist/components/ui/slider/index.js +113 -0
  87. package/dist/components/ui/slider/utils.d.ts +16 -0
  88. package/dist/components/ui/slider/utils.js +56 -0
  89. package/dist/components/ui/spinner/index.d.ts +42 -0
  90. package/dist/components/ui/spinner/index.figma.d.ts +1 -0
  91. package/dist/components/ui/spinner/index.figma.js +16 -0
  92. package/dist/components/ui/spinner/index.js +51 -0
  93. package/dist/components/ui/switch/index.d.ts +46 -0
  94. package/dist/components/ui/switch/index.figma.d.ts +1 -0
  95. package/dist/components/ui/switch/index.figma.js +27 -0
  96. package/dist/components/ui/switch/index.js +75 -0
  97. package/dist/components/ui/tabs/index.d.ts +53 -0
  98. package/dist/components/ui/tabs/index.figma.d.ts +1 -0
  99. package/dist/components/ui/tabs/index.figma.js +55 -0
  100. package/dist/components/ui/tabs/index.js +164 -0
  101. package/dist/components/ui/tag/index.d.ts +50 -0
  102. package/dist/components/ui/tag/index.figma.d.ts +1 -0
  103. package/dist/components/ui/tag/index.figma.js +32 -0
  104. package/dist/components/ui/tag/index.js +177 -0
  105. package/dist/components/ui/textarea/index.d.ts +49 -0
  106. package/dist/components/ui/textarea/index.figma.d.ts +1 -0
  107. package/dist/components/ui/textarea/index.figma.js +27 -0
  108. package/dist/components/ui/textarea/index.js +94 -0
  109. package/dist/components/ui/toast/index.d.ts +60 -0
  110. package/dist/components/ui/toast/index.figma.d.ts +1 -0
  111. package/dist/components/ui/toast/index.figma.js +26 -0
  112. package/dist/components/ui/toast/index.js +57 -0
  113. package/dist/components/ui/tooltip/index.d.ts +52 -0
  114. package/dist/components/ui/tooltip/index.figma.d.ts +1 -0
  115. package/dist/components/ui/tooltip/index.figma.js +23 -0
  116. package/dist/components/ui/tooltip/index.js +65 -0
  117. package/dist/index.d.ts +27 -0
  118. package/dist/index.js +27 -0
  119. package/dist/lib/utils.d.ts +2 -0
  120. package/dist/lib/utils.js +5 -0
  121. package/dist/test/helpers.d.ts +56 -0
  122. package/dist/test/helpers.js +182 -0
  123. package/dist/test/setup.d.ts +1 -0
  124. package/dist/test/setup.js +38 -0
  125. package/package.json +253 -0
@@ -0,0 +1,154 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ /**
14
+ * Copyright 2026 Goodpatch Inc.
15
+ * SPDX-License-Identifier: Apache-2.0
16
+ */
17
+ import * as React from "react";
18
+ import { cva } from "class-variance-authority";
19
+ import { Icon } from "../../../components/ui/icon/index.js";
20
+ import { IconButton } from "../../../components/ui/icon-button/index.js";
21
+ import { cn } from "../../../lib/utils.js";
22
+ // ステータスごとのスタイル定義
23
+ const inlineMessageVariants = cva("relative w-[448px] max-w-full rounded-md border p-3 flex gap-2 items-start", {
24
+ variants: {
25
+ status: {
26
+ info: "bg-info-50 border-info-300",
27
+ warning: "bg-warning-50 border-warning-200",
28
+ negative: "bg-negative-50 border-negative-300",
29
+ success: "bg-success-50 border-success-300",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ status: "info",
34
+ },
35
+ });
36
+ // ステータスごとのアイコン名
37
+ const statusIcons = {
38
+ info: "info",
39
+ warning: "warning",
40
+ negative: "error",
41
+ success: "check_circle",
42
+ };
43
+ // ステータスごとの色クラス
44
+ const statusColorClasses = {
45
+ info: "text-info-400",
46
+ warning: "text-warning-500",
47
+ negative: "text-negative-400",
48
+ success: "text-success-400",
49
+ };
50
+ const InlineMessageContext = React.createContext(null);
51
+ /**
52
+ * **概要 / Overview**
53
+ *
54
+ * - インラインメッセージはユーザーにシステムやサービスの状態を伝えるために使用するコンポーネントです。
55
+ * - en: The InlineMessage component is used to convey system or service status to users.
56
+ *
57
+ * **使用例 / Usage Example**
58
+ *
59
+ * ```tsx
60
+ * <InlineMessage status="info" onClose={() => console.log('closed')}>
61
+ * <InlineMessageTitle>情報</InlineMessageTitle>
62
+ * <InlineMessageDescription>
63
+ * これは情報メッセージです。
64
+ * </InlineMessageDescription>
65
+ * </InlineMessage>
66
+ * ```
67
+ *
68
+ * @param {InlineMessageProps} props
69
+ */
70
+ const InlineMessage = React.forwardRef((_a, ref) => {
71
+ var { className, status = "info", onClose, isCloseTrigger = true, children } = _a, props = __rest(_a, ["className", "status", "onClose", "isCloseTrigger", "children"]);
72
+ // 一意なIDを生成 / en: Generate unique IDs
73
+ const baseId = React.useId();
74
+ const titleId = `${baseId}-title`;
75
+ const descId = `${baseId}-desc`;
76
+ // 子コンポーネントの登録状態 / en: Registration state of child components
77
+ const [hasTitle, setHasTitle] = React.useState(false);
78
+ const [hasDesc, setHasDesc] = React.useState(false);
79
+ // 子コンポーネントが自分を登録するコールバック
80
+ // en: Callbacks for child components to register themselves
81
+ const registerTitle = React.useCallback(() => {
82
+ setHasTitle(true);
83
+ return () => setHasTitle(false);
84
+ }, []);
85
+ const registerDesc = React.useCallback(() => {
86
+ setHasDesc(true);
87
+ return () => setHasDesc(false);
88
+ }, []);
89
+ // Context値をメモ化 / en: Memoize context value
90
+ const contextValue = React.useMemo(() => ({ titleId, descId, registerTitle, registerDesc }), [titleId, descId, registerTitle, registerDesc]);
91
+ // ステータスに対応するアイコン情報の取得
92
+ const statusIcon = status ? statusIcons[status] : statusIcons.info;
93
+ // ステータスに対応する色クラスの取得
94
+ const colorClass = status
95
+ ? statusColorClasses[status]
96
+ : statusColorClasses.info;
97
+ return (_jsx(InlineMessageContext.Provider, { value: contextValue, children: _jsxs("div", Object.assign({ ref: ref, role: "alert", "aria-labelledby": hasTitle ? titleId : undefined, "aria-describedby": hasDesc ? descId : undefined, className: cn(inlineMessageVariants({ status }), className) }, props, { children: [_jsx("span", { className: "inline-flex p-1 items-center justify-center", "aria-hidden": "true", children: _jsx(Icon, { icon: statusIcon, size: 6, fill: false, className: cn(colorClass) }) }), _jsx("div", { className: "flex flex-col flex-1 min-h-8", children: children }), isCloseTrigger && onClose && (_jsx(IconButton, { icon: "close", size: "sm", variant: "ghost", theme: "neutral", onClick: onClose, "aria-label": "\u9589\u3058\u308B", className: "shrink-0" }))] })) }));
98
+ });
99
+ InlineMessage.displayName = "InlineMessage";
100
+ /**
101
+ * **概要 / Overview**
102
+ *
103
+ * - インラインメッセージのタイトルコンポーネントです。
104
+ * - en: Title component for inline messages.
105
+ *
106
+ * **使用例 / Usage Example**
107
+ *
108
+ * ```tsx
109
+ * <InlineMessageTitle>タイトル</InlineMessageTitle>
110
+ * ```
111
+ *
112
+ * @param {React.HTMLAttributes<HTMLSpanElement>} props
113
+ */
114
+ const InlineMessageTitle = React.forwardRef((_a, ref) => {
115
+ var { className } = _a, props = __rest(_a, ["className"]);
116
+ const context = React.useContext(InlineMessageContext);
117
+ // マウント時に親へ登録、アンマウント時に解除
118
+ // en: Register on mount, unregister on unmount
119
+ React.useEffect(() => {
120
+ if (!context)
121
+ return;
122
+ return context.registerTitle();
123
+ }, [context]);
124
+ return (_jsx("div", { className: cn("flex items-center min-h-8"), children: _jsx("span", Object.assign({ ref: ref, id: context === null || context === void 0 ? void 0 : context.titleId, className: cn("character-3-bold-pro text-text-high", className) }, props)) }));
125
+ });
126
+ InlineMessageTitle.displayName = "InlineMessageTitle";
127
+ /**
128
+ * **概要 / Overview**
129
+ *
130
+ * - インラインメッセージの説明文コンポーネントです。
131
+ * - en: Description component for inline messages.
132
+ *
133
+ * **使用例 / Usage Example**
134
+ *
135
+ * ```tsx
136
+ * <InlineMessageDescription>詳細な説明文</InlineMessageDescription>
137
+ * ```
138
+ *
139
+ * @param {React.HTMLAttributes<HTMLParagraphElement>} props
140
+ */
141
+ const InlineMessageDescription = React.forwardRef((_a, ref) => {
142
+ var { className } = _a, props = __rest(_a, ["className"]);
143
+ const context = React.useContext(InlineMessageContext);
144
+ // マウント時に親へ登録、アンマウント時に解除
145
+ // en: Register on mount, unregister on unmount
146
+ React.useEffect(() => {
147
+ if (!context)
148
+ return;
149
+ return context.registerDesc();
150
+ }, [context]);
151
+ return (_jsx("div", { className: cn("flex items-center min-h-8"), children: _jsx("p", Object.assign({ ref: ref, id: context === null || context === void 0 ? void 0 : context.descId, className: cn("character-3-regular-pro text-text-middle", className) }, props)) }));
152
+ });
153
+ InlineMessageDescription.displayName = "InlineMessageDescription";
154
+ export { InlineMessage, InlineMessageTitle, InlineMessageDescription };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Copyright 2026 Goodpatch Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import * as React from "react";
6
+ import { type VariantProps } from "class-variance-authority";
7
+ declare const inputVariants: (props?: ({
8
+ size?: "sm" | "md" | "lg" | null | undefined;
9
+ isInvalid?: boolean | null | undefined;
10
+ isDisabled?: boolean | null | undefined;
11
+ isFocused?: boolean | null | undefined;
12
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
13
+ type InputVariantProps = VariantProps<typeof inputVariants>;
14
+ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
15
+ /**
16
+ * Inputサイズ指定
17
+ * en: Input size specification
18
+ */
19
+ size?: InputVariantProps["size"];
20
+ /**
21
+ * 無効にするかどうか
22
+ * en: Whether to disable the input
23
+ */
24
+ isDisabled?: boolean;
25
+ /**
26
+ * フォーカスするかどうか
27
+ * en: Whether to focus the input
28
+ */
29
+ isFocused?: boolean;
30
+ /**
31
+ * フィールドが無効かどうか
32
+ * en: Whether the field is invalid
33
+ */
34
+ isInvalid?: boolean;
35
+ /**
36
+ * アイコンボタンを有効にするフラグ
37
+ * en: Flag to enable the icon button
38
+ */
39
+ isTrigger?: boolean;
40
+ /**
41
+ * ボタンに表示するアイコン名
42
+ * en: Icon name displayed in the button
43
+ */
44
+ triggerIcon?: string;
45
+ /**
46
+ * アイコンボタンのアクセシビリティラベル
47
+ * en: Accessibility label for the icon button
48
+ */
49
+ triggerAriaLabel?: string;
50
+ /**
51
+ * アイコンボタンクリック時のコールバック
52
+ * en: Callback function for icon button click
53
+ */
54
+ onIconButtonClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
55
+ }
56
+ /**
57
+ * **概要 / Overview**
58
+ *
59
+ * - インプットはテキストフィールドの形式でユーザーからの入力を取得するために使用するコンポーネントです。
60
+ * - en: The Input component is used to capture user input in the form of a text field.
61
+ *
62
+ * **アンチパターン / Anti-patterns**
63
+ *
64
+ * - Input の横にアイコンボタンを手動で配置しないでください。`isTrigger` / `triggerIcon` props を使用してください。
65
+ * en: Do not manually place an IconButton next to Input. Use `isTrigger` / `triggerIcon` props instead.
66
+ * - Input と横並びの Button は原則同じサイズにしてください。デフォルトの Input には `Button size="md"`、省スペース UI では `Input size="sm"` と `Button size="sm"` を使ってください。
67
+ * en: Keep Button size aligned when placing it next to Input. Use `Button size="md"` with the default Input, or pair `Input size="sm"` with `Button size="sm"` in compact UIs.
68
+ * - 無効状態は `isDisabled` を優先して使ってください。HTML 標準の `disabled` も互換のため受け付けますが、Sparkle Design のコードでは `isDisabled` に統一します。
69
+ * en: Prefer `isDisabled` for disabled state. The native `disabled` prop is still supported for compatibility, but Sparkle Design code should standardize on `isDisabled`.
70
+ *
71
+ * ```tsx
72
+ * // ✅ Correct
73
+ * <>
74
+ * <Input
75
+ * isTrigger
76
+ * triggerIcon="search"
77
+ * triggerAriaLabel="検索"
78
+ * onIconButtonClick={() => {}}
79
+ * />
80
+ * <div className="flex gap-2">
81
+ * <Input placeholder="検索..." />
82
+ * <Button size="md">検索</Button>
83
+ * </div>
84
+ * <div className="flex gap-2">
85
+ * <Input size="sm" placeholder="メッセージ" />
86
+ * <Button size="sm">送信</Button>
87
+ * </div>
88
+ * <Input isDisabled placeholder="無効状態" />
89
+ * </>
90
+ *
91
+ * // ❌ Wrong - 手動配置
92
+ * <div className="flex">
93
+ * <Input />
94
+ * <IconButton icon="search" aria-label="検索" />
95
+ * </div>
96
+ *
97
+ * // ❌ Wrong - サイズ不一致
98
+ * <div className="flex gap-2">
99
+ * <Input placeholder="検索..." />
100
+ * <Button size="sm">検索</Button>
101
+ * </div>
102
+ * ```
103
+
104
+ * **使用例 / Usage Example**
105
+ *
106
+ * ```tsx
107
+ * <Input
108
+ * size="md"
109
+ * placeholder="テキストを入力"
110
+ * isTrigger
111
+ * triggerIcon="search"
112
+ * />
113
+ * ```
114
+ *
115
+ * @param {InputProps} props
116
+ */
117
+ declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
118
+ export { Input, inputVariants };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import figma from "@figma/code-connect";
3
+ import { Input } from "./index.js";
4
+ /**
5
+ * -- This file was auto-generated by Code Connect --
6
+ * `props` includes a mapping from your code props to Figma properties.
7
+ * You should check this is correct, and update the `example` function
8
+ * to return the code example you'd like to see in Figma
9
+ */
10
+ figma.connect(Input, "https://www.figma.com/design/7alBZXZf65YgcII41TWT0r/Sparkle-Design?node-id=1274%3A63045", {
11
+ props: {
12
+ // These props were automatically mapped based on your linked code:
13
+ size: figma.enum("size", {
14
+ sm: "sm",
15
+ md: "md",
16
+ lg: "lg",
17
+ }),
18
+ isDisabled: figma.enum("state", {
19
+ disabled: true,
20
+ }),
21
+ // isFocused: figma.boolean("isFocused"),
22
+ isInvalid: figma.boolean("isInvalid"),
23
+ defaultValue: figma.boolean("isEmpty", {
24
+ true: undefined,
25
+ false: figma.string("value"),
26
+ }),
27
+ placeholder: figma.boolean("isEmpty", {
28
+ true: figma.string("value"),
29
+ false: undefined,
30
+ }),
31
+ // No matching props could be found for these Figma properties:
32
+ isTrigger: figma.boolean("isTrigger"),
33
+ // "isFocused": figma.boolean('isFocused'),
34
+ },
35
+ example: props => (_jsx(Input, { size: props.size, isDisabled: props.isDisabled, isInvalid: props.isInvalid, defaultValue: props.defaultValue, isTrigger: props.isTrigger, placeholder: props.placeholder })),
36
+ });
@@ -0,0 +1,256 @@
1
+ var __rest = (this && this.__rest) || function (s, e) {
2
+ var t = {};
3
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
4
+ t[p] = s[p];
5
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
6
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
7
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
8
+ t[p[i]] = s[p[i]];
9
+ }
10
+ return t;
11
+ };
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ /**
14
+ * Copyright 2026 Goodpatch Inc.
15
+ * SPDX-License-Identifier: Apache-2.0
16
+ */
17
+ import * as React from "react";
18
+ import { cva } from "class-variance-authority";
19
+ import { cn } from "../../../lib/utils.js";
20
+ import { IconButton } from "../../../components/ui/icon-button/index.js";
21
+ // 入力フィールドのスタイル定義
22
+ const inputVariants = cva("flex gap-0 items-center w-full rounded-action border bg-white transition-colors p-1", {
23
+ variants: {
24
+ size: {
25
+ sm: "h-8 character-2-regular-pro",
26
+ md: "h-10 character-3-regular-pro",
27
+ lg: "h-12 character-4-regular-pro",
28
+ },
29
+ isInvalid: {
30
+ true: "border-negative-500",
31
+ false: "border-neutral-500",
32
+ },
33
+ isDisabled: {
34
+ true: "cursor-not-allowed",
35
+ false: "",
36
+ },
37
+ isFocused: {
38
+ true: "ring-2 ring-[var(--color-ring-normal)] ring-offset-2 outline-hidden",
39
+ false: "",
40
+ },
41
+ },
42
+ compoundVariants: [
43
+ // 通常状態
44
+ {
45
+ isInvalid: false,
46
+ isDisabled: false,
47
+ className: "border-neutral-500 hover:border-neutral-600",
48
+ },
49
+ // エラー状態
50
+ {
51
+ isInvalid: true,
52
+ isDisabled: false,
53
+ className: "border-negative-500 hover:border-negative-600 bg-white",
54
+ },
55
+ // 無効状態
56
+ {
57
+ isInvalid: false,
58
+ isDisabled: true,
59
+ className: "border-neutral-200 bg-neutral-50",
60
+ },
61
+ // エラー+無効状態
62
+ {
63
+ isInvalid: true,
64
+ isDisabled: true,
65
+ className: "border-negative-200 bg-neutral-50",
66
+ },
67
+ ],
68
+ defaultVariants: {
69
+ size: "md",
70
+ isInvalid: false,
71
+ isDisabled: false,
72
+ isFocused: false,
73
+ },
74
+ });
75
+ // 複数のrefをマージするユーティリティ関数
76
+ function useMergeRefs(...refs) {
77
+ return React.useCallback((value) => {
78
+ refs.forEach(ref => {
79
+ if (typeof ref === "function") {
80
+ ref(value);
81
+ }
82
+ else if (ref != null) {
83
+ ref.current = value;
84
+ }
85
+ });
86
+ }, [refs]);
87
+ }
88
+ /**
89
+ * **概要 / Overview**
90
+ *
91
+ * - インプットはテキストフィールドの形式でユーザーからの入力を取得するために使用するコンポーネントです。
92
+ * - en: The Input component is used to capture user input in the form of a text field.
93
+ *
94
+ * **アンチパターン / Anti-patterns**
95
+ *
96
+ * - Input の横にアイコンボタンを手動で配置しないでください。`isTrigger` / `triggerIcon` props を使用してください。
97
+ * en: Do not manually place an IconButton next to Input. Use `isTrigger` / `triggerIcon` props instead.
98
+ * - Input と横並びの Button は原則同じサイズにしてください。デフォルトの Input には `Button size="md"`、省スペース UI では `Input size="sm"` と `Button size="sm"` を使ってください。
99
+ * en: Keep Button size aligned when placing it next to Input. Use `Button size="md"` with the default Input, or pair `Input size="sm"` with `Button size="sm"` in compact UIs.
100
+ * - 無効状態は `isDisabled` を優先して使ってください。HTML 標準の `disabled` も互換のため受け付けますが、Sparkle Design のコードでは `isDisabled` に統一します。
101
+ * en: Prefer `isDisabled` for disabled state. The native `disabled` prop is still supported for compatibility, but Sparkle Design code should standardize on `isDisabled`.
102
+ *
103
+ * ```tsx
104
+ * // ✅ Correct
105
+ * <>
106
+ * <Input
107
+ * isTrigger
108
+ * triggerIcon="search"
109
+ * triggerAriaLabel="検索"
110
+ * onIconButtonClick={() => {}}
111
+ * />
112
+ * <div className="flex gap-2">
113
+ * <Input placeholder="検索..." />
114
+ * <Button size="md">検索</Button>
115
+ * </div>
116
+ * <div className="flex gap-2">
117
+ * <Input size="sm" placeholder="メッセージ" />
118
+ * <Button size="sm">送信</Button>
119
+ * </div>
120
+ * <Input isDisabled placeholder="無効状態" />
121
+ * </>
122
+ *
123
+ * // ❌ Wrong - 手動配置
124
+ * <div className="flex">
125
+ * <Input />
126
+ * <IconButton icon="search" aria-label="検索" />
127
+ * </div>
128
+ *
129
+ * // ❌ Wrong - サイズ不一致
130
+ * <div className="flex gap-2">
131
+ * <Input placeholder="検索..." />
132
+ * <Button size="sm">検索</Button>
133
+ * </div>
134
+ * ```
135
+
136
+ * **使用例 / Usage Example**
137
+ *
138
+ * ```tsx
139
+ * <Input
140
+ * size="md"
141
+ * placeholder="テキストを入力"
142
+ * isTrigger
143
+ * triggerIcon="search"
144
+ * />
145
+ * ```
146
+ *
147
+ * @param {InputProps} props
148
+ */
149
+ const Input = React.forwardRef((_a, ref) => {
150
+ var { className, size, isInvalid = false, isDisabled = false, isTrigger = false, triggerIcon = "edit", triggerAriaLabel, onIconButtonClick, disabled, defaultValue, value, onChange, onBlur, onFocus } = _a, props = __rest(_a, ["className", "size", "isInvalid", "isDisabled", "isTrigger", "triggerIcon", "triggerAriaLabel", "onIconButtonClick", "disabled", "defaultValue", "value", "onChange", "onBlur", "onFocus"]);
151
+ // Refs
152
+ const containerRef = React.useRef(null);
153
+ const buttonRef = React.useRef(null);
154
+ const inputRef = React.useRef(null);
155
+ const mergedInputRef = useMergeRefs(inputRef, ref);
156
+ // 状態管理
157
+ const [isInputFocused, setIsInputFocused] = React.useState(false);
158
+ const [isIconButtonFocused, setIsIconButtonFocused] = React.useState(false);
159
+ // HTML標準のdisabled属性とisDisabledプロパティを組み合わせた実際の無効状態
160
+ const isInputDisabled = Boolean(isDisabled || disabled);
161
+ // 入力値が変更されたときの処理
162
+ const handleChange = React.useCallback((e) => {
163
+ if (isInputDisabled)
164
+ return;
165
+ onChange === null || onChange === void 0 ? void 0 : onChange(e);
166
+ }, [onChange, isInputDisabled]);
167
+ // Input要素のフォーカス処理
168
+ const handleInputFocus = React.useCallback((e) => {
169
+ if (isInputDisabled)
170
+ return;
171
+ setIsInputFocused(true);
172
+ onFocus === null || onFocus === void 0 ? void 0 : onFocus(e);
173
+ }, [isInputDisabled, onFocus]);
174
+ const handleInputBlur = React.useCallback((e) => {
175
+ // ボタンにフォーカスが移った場合は、入力欄のフォーカスは解除しない
176
+ if (buttonRef.current !== e.relatedTarget) {
177
+ setIsInputFocused(false);
178
+ }
179
+ onBlur === null || onBlur === void 0 ? void 0 : onBlur(e);
180
+ }, [onBlur]);
181
+ // ボタンのフォーカス処理
182
+ const handleIconButtonFocus = React.useCallback(() => {
183
+ if (isInputDisabled)
184
+ return;
185
+ setIsIconButtonFocused(true);
186
+ setIsInputFocused(false);
187
+ }, [isInputDisabled]);
188
+ const handleIconButtonBlur = React.useCallback((e) => {
189
+ // 入力欄にフォーカスが移った場合は、ボタンのフォーカスは解除しない
190
+ if (inputRef.current !== e.relatedTarget) {
191
+ setIsIconButtonFocused(false);
192
+ }
193
+ }, []);
194
+ // コンテナクリック時にインプットにフォーカスを当てる
195
+ const handleContainerClick = React.useCallback((e) => {
196
+ if (isInputDisabled)
197
+ return;
198
+ // アイコンボタン上でのクリックを除外
199
+ if (buttonRef.current &&
200
+ (buttonRef.current === e.target ||
201
+ buttonRef.current.contains(e.target))) {
202
+ return;
203
+ }
204
+ // 入力要素が無効でなければフォーカスを当てる
205
+ if (inputRef.current) {
206
+ inputRef.current.focus();
207
+ }
208
+ }, [isInputDisabled]);
209
+ // 外部クリックでフォーカスを解除するためのハンドラ
210
+ React.useEffect(() => {
211
+ // 無効状態の場合はイベントリスナーを追加しない
212
+ if (isInputDisabled)
213
+ return;
214
+ const handleOutsideClick = (e) => {
215
+ // コンポーネント外のクリックを検出
216
+ if (containerRef.current &&
217
+ !containerRef.current.contains(e.target)) {
218
+ setIsInputFocused(false);
219
+ setIsIconButtonFocused(false);
220
+ }
221
+ };
222
+ // イベントリスナーを追加
223
+ document.addEventListener("mousedown", handleOutsideClick);
224
+ // クリーンアップ関数
225
+ return () => {
226
+ document.removeEventListener("mousedown", handleOutsideClick);
227
+ };
228
+ }, [isInputDisabled]);
229
+ // ボタンサイズの計算
230
+ const iconButtonSize = React.useMemo(() => {
231
+ switch (size) {
232
+ case "sm":
233
+ return "xs";
234
+ case "lg":
235
+ return "md";
236
+ default:
237
+ return "sm";
238
+ }
239
+ }, [size]);
240
+ return (_jsxs("div", { ref: containerRef, className: cn(inputVariants({
241
+ size,
242
+ isInvalid,
243
+ isDisabled: isInputDisabled,
244
+ isFocused: isInputFocused && !isIconButtonFocused,
245
+ className,
246
+ }), !isInputDisabled && "cursor-text" // 入力可能な場合はテキストカーソルを表示
247
+ ),
248
+ // NOTE: not supportエラーがLintで出るためコメントアウト
249
+ // aria-disabled={isInputDisabled}
250
+ // aria-invalid={isInvalid === null ? undefined : isInvalid}
251
+ onClick: handleContainerClick, role: "presentation", tabIndex: -1, children: [_jsx("input", Object.assign({ ref: mergedInputRef, disabled: isInputDisabled, "aria-invalid": isInvalid || undefined, className: cn("w-full h-full bg-transparent border-none outline-hidden focus:outline-hidden", "text-text-high placeholder:text-text-placeholder px-2", isInputDisabled &&
252
+ "cursor-not-allowed text-neutral-400 placeholder:text-text-disabled"), onChange: handleChange, onFocus: handleInputFocus, onBlur: handleInputBlur, defaultValue: defaultValue, value: value, "aria-disabled": isInputDisabled }, props)), isTrigger && (_jsx(IconButton, { ref: buttonRef, icon: triggerIcon, theme: "neutral", variant: "ghost", size: iconButtonSize, onClick: onIconButtonClick, isDisabled: isInputDisabled, disabled: isInputDisabled, type: "button" // フォーム内でデフォルトのsubmit動作を防ぐ
253
+ , "aria-label": triggerAriaLabel, onFocus: handleIconButtonFocus, onBlur: handleIconButtonBlur }))] }));
254
+ });
255
+ Input.displayName = "Input";
256
+ export { Input, inputVariants };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Copyright 2026 Goodpatch Inc.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ import * as React from "react";
6
+ import { type InputProps } from "@/components/ui/input";
7
+ export interface InputPasswordProps extends Omit<InputProps, "type" | "triggerIcon" | "triggerAriaLabel" | "isTrigger" | "onIconButtonClick"> {
8
+ }
9
+ /**
10
+ * **概要 / Overview**
11
+ *
12
+ * - インプットパスワードはパスワードの情報をユーザーから取得するために使用するコンポーネントです。
13
+ * - en: The InputPassword component is used to capture password information from users.
14
+ *
15
+ * **使用例 / Usage Example**
16
+ *
17
+ * ```tsx
18
+ * <InputPassword
19
+ * size="md"
20
+ * placeholder="パスワードを入力してください"
21
+ * />
22
+ * ```
23
+ *
24
+ * @param {InputPasswordProps} props
25
+ */
26
+ declare const InputPassword: React.ForwardRefExoticComponent<InputPasswordProps & React.RefAttributes<HTMLInputElement>>;
27
+ export { InputPassword };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import figma from "@figma/code-connect";
3
+ import { InputPassword } from "./index.js";
4
+ /**
5
+ * -- This file was auto-generated by Code Connect --
6
+ * `props` includes a mapping from your code props to Figma properties.
7
+ * You should check this is correct, and update the `example` function
8
+ * to return the code example you'd like to see in Figma
9
+ */
10
+ figma.connect(InputPassword, "https://www.figma.com/design/7alBZXZf65YgcII41TWT0r/Sparkle-Design?node-id=174%3A3296", {
11
+ props: {
12
+ // These props were automatically mapped based on your linked code:
13
+ size: figma.enum("size", {
14
+ sm: "sm",
15
+ md: "md",
16
+ lg: "lg",
17
+ }),
18
+ disabled: figma.enum("state", {
19
+ disable: true,
20
+ }),
21
+ isFocused: figma.boolean("isFocused"),
22
+ isInvalid: figma.boolean("isInvaild"),
23
+ defaultValue: figma.boolean("isEmpty", {
24
+ true: undefined,
25
+ false: figma.string("value"),
26
+ }),
27
+ placeholder: figma.boolean("isEmpty", {
28
+ true: figma.string("value"),
29
+ false: undefined,
30
+ }),
31
+ // No matching props could be found for these Figma properties:
32
+ // "isFocused": figma.boolean('isFocused'),
33
+ // "value": figma.string('value')
34
+ },
35
+ example: props => (_jsx(InputPassword, { size: props.size, disabled: props.disabled, isFocused: props.isFocused, isInvalid: props.isInvalid, defaultValue: props.defaultValue, placeholder: props.placeholder })),
36
+ });
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Copyright 2026 Goodpatch Inc.
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import * as React from "react";
7
+ import { Input } from "../../../components/ui/input/index.js";
8
+ /**
9
+ * **概要 / Overview**
10
+ *
11
+ * - インプットパスワードはパスワードの情報をユーザーから取得するために使用するコンポーネントです。
12
+ * - en: The InputPassword component is used to capture password information from users.
13
+ *
14
+ * **使用例 / Usage Example**
15
+ *
16
+ * ```tsx
17
+ * <InputPassword
18
+ * size="md"
19
+ * placeholder="パスワードを入力してください"
20
+ * />
21
+ * ```
22
+ *
23
+ * @param {InputPasswordProps} props
24
+ */
25
+ const InputPassword = React.forwardRef((props, ref) => {
26
+ // パスワードの表示状態を管理
27
+ const [isVisibility, setIsVisibility] = React.useState(false);
28
+ // パスワード表示切り替えハンドラー
29
+ const handleTogglePasswordVisibility = React.useCallback(() => {
30
+ setIsVisibility(prev => !prev);
31
+ }, []);
32
+ return (_jsx(Input, Object.assign({ ref: ref, type: isVisibility ? "text" : "password", triggerIcon: isVisibility ? "visibility_off" : "visibility", triggerAriaLabel: isVisibility ? "パスワードを隠す" : "パスワードを表示する", isTrigger: true, onIconButtonClick: handleTogglePasswordVisibility }, props)));
33
+ });
34
+ InputPassword.displayName = "InputPassword";
35
+ export { InputPassword };