Skip to content

Commit 7d445ad

Browse files
committed
upd(inkButton): rename type icon to square, add prop icon
1 parent 548ab4b commit 7d445ad

File tree

10 files changed

+169
-59
lines changed

10 files changed

+169
-59
lines changed

.changeset/proud-baths-swim.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@inkcre/web-design": patch
3+
---
4+
5+
upd(InkButton): support prop `icon` and rename `icon` type to `square`

packages/web-design/agent-skills/components/references/inkButton.md

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@ Use it for any single-click action that requires a compact button UI; avoid usin
1414

1515
### Concepts
1616

17-
- `type`: visual and purposeful variant of the button (`subtle`, `primary`, `danger`).
17+
- `theme`: visual and purposeful variant of the button (`subtle`, `primary`, `danger`).
18+
- `type`: shape variant of the button (`default`, `square`).
1819
- `size`: density of the control (`md`, `sm`).
20+
- `icon`: iconfont class name (e.g., `i-mdi-cog`).
21+
- `iconPlacement`: where to place the icon (`prefix`, `suffix`).
22+
- `isLoading`: whether the button is in a loading state.
1923
- `slot`: custom content (e.g., icon + label, label only).
2024

2125
### Visual / UX Meaning
@@ -24,6 +28,7 @@ Use it for any single-click action that requires a compact button UI; avoid usin
2428
- `primary`: main action in a section or modal; higher contrast to attract attention.
2529
- `danger`: destructive action that explains a potentially destructive outcome (e.g., delete).
2630
- `sm`: smaller footprint for compact toolbars; `md`: default size used in forms and actions.
31+
- `square`: used for icon-only buttons.
2732

2833
## Canonical Examples
2934

@@ -51,39 +56,50 @@ Use it for any single-click action that requires a compact button UI; avoid usin
5156
<InkButton text="Edit" size="sm" />
5257
```
5358

59+
- Icon: Use built-in icon prop.
60+
61+
```vue
62+
<InkButton text="Settings" icon="i-mdi-cog" />
63+
```
64+
65+
- Loading: Show loading state.
66+
67+
```vue
68+
<InkButton text="Saving..." :isLoading="true" />
69+
```
70+
5471
### Custom slot content (icon + label)
5572

5673
The `slot` is supported for custom content; this is how you add icons or non-text children.
5774

5875
```vue
5976
<InkButton theme="primary">
60-
<svg aria-hidden="true" width="16" height="16">...</svg>
77+
<div class="i-mdi-content-save" />
6178
<span>Save</span>
62-
</InkButton>
79+
</InkButton>
6380
```
6481

6582
## Behavioral Contract
6683

6784
- In all variants, clicking the button emits a `click` event.
68-
- Click events are always emitted; the component does not internally debounce, block, or prevent repeated clicks.
69-
- The default values are: `text` = "Button Text", `type` = `subtle`, `size` = `md`.
70-
- The component uses a native `<button>` element, so it inherits browser keyboard accessibility (space/enter) and form behavior; consumers should be aware of the native `type` default behavior in forms and add an explicit `type` attribute where necessary to avoid accidental form submits.
85+
- Click events are NOT emitted when `isLoading` is true.
86+
- The default values are: `text` = "Button Text", `theme` = `subtle`, `type` = `default`, `size` = `md`.
87+
- The component uses a native `<button>` element, so it inherits browser keyboard accessibility (space/enter) and form behavior.
7188

7289
## Extension & Composition
7390

7491
- The component is intentionally simple and is designed for composition via the `slot` and surrounding layout.
75-
- Accepts custom slot content for icons or complex inline labels (icon + text). The component does not provide a built-in `icon` prop; use the slot to place an icon element before or after the label.
76-
- Works well inside `FormItem` or other containers. Because it is a native `button`, it behaves like a regular HTML button in forms.
92+
- Accepts custom slot content for icons or complex inline labels (icon + text).
93+
- Works well inside `FormItem` or other containers.
7794

7895
## Non-Goals
7996

80-
- There is no built-in `disabled` or `loading` prop. Disabling or preventing repeated action should be handled by parent components or by a wrapper that manages state.
8197
- It does not provide variant-level keyboard shortcuts or automatic confirmation dialogs.
82-
- It does not manage long-running workflows, API states, or request lifecycle (for example, showing an internal spinner when an action is in-flight).
98+
- It does not manage long-running workflows, API states, or request lifecycle (it only provides the `isLoading` visual state).
8399

84100
## Implementation Notes
85101

86-
- Props: `text` (string, default "Button Text"), `type` (`subtle` | `primary` | `danger`, default `subtle`), `size` (`md` | `sm`, default `md`).
102+
- Props: `text` (string, default "Button Text"), `theme` (`subtle` | `primary` | `danger`, default `subtle`), `type` (`default` | `square`, default `default`), `size` (`md` | `sm`, default `md`), `icon` (string), `iconPlacement` (`prefix` | `suffix`, default `prefix`), `isLoading` (boolean, default `false`).
87103
- Emits: `click` (always true), implemented as a regular native `<button>` click event via `emit('click')`.
88104
- The `slot` is present and should be considered the source of truth for custom content; the `text` prop is a convenience when only a label is used.
89105
- Accessibility note for maintainers: the component omits an explicit `type` attribute on the underlying `<button>`, so by default in HTML forms the button will behave as `submit`. When used inside a form where submission is not intended, the caller should pass a `type="button"` attribute to avoid accidental form submits.
@@ -92,7 +108,10 @@ The `slot` is supported for custom content; this is how you add icons or non-tex
92108

93109
```typescript
94110
export const inkButtonProps = {
95-
text: makeStringProp("Button Text"),
111+
text: makeStringProp(),
112+
/** iconfont class name, eg. i-mdi-menu */
113+
icon: makeStringProp(),
114+
iconPlacement: makeStringProp<"prefix" | "suffix">("prefix"),
96115
type: makeStringProp<ButtonType>("default"),
97116
theme: makeStringProp<ButtonTheme>("subtle"),
98117
size: makeStringProp<ButtonSize>("md"),
@@ -112,12 +131,15 @@ export const inkButtonEmits = {
112131

113132
```typescript
114133
type ButtonTheme = "subtle" | "primary" | "danger";
115-
type ButtonType = "default" | "icon";
134+
type ButtonType = "default" | "square";
116135
type ButtonSize = "md" | "sm";
117136

118137
// --- Props ---
119138
export const inkButtonProps = {
120-
text: makeStringProp("Button Text"),
139+
text: makeStringProp(),
140+
/** iconfont class name, eg. i-mdi-menu */
141+
icon: makeStringProp(),
142+
iconPlacement: makeStringProp<"prefix" | "suffix">("prefix"),
121143
type: makeStringProp<ButtonType>("default"),
122144
theme: makeStringProp<ButtonTheme>("subtle"),
123145
size: makeStringProp<ButtonSize>("md"),

packages/web-design/src/components/inkButton/inkButton.scss

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
position: relative;
1313
box-sizing: border-box;
1414

15+
// State styles
1516
&__loading-overlay {
1617
position: absolute;
1718
top: 0;
@@ -35,7 +36,7 @@
3536
}
3637

3738
// Theme variants
38-
&--subtle {
39+
&--theme-subtle {
3940
border: 1px solid sys-var(color, border, base);
4041
background-color: sys-var(color, surface, subtle);
4142
color: sys-var(color, text, base);
@@ -46,7 +47,7 @@
4647
}
4748
}
4849

49-
&--primary {
50+
&--theme-primary {
5051
background-color: sys-var(color, surface, primary);
5152
color: sys-var(color, text, primary);
5253

@@ -56,7 +57,7 @@
5657
}
5758
}
5859

59-
&--danger {
60+
&--theme-danger {
6061
background-color: sys-var(color, surface, danger);
6162
color: sys-var(color, text, danger-on);
6263

@@ -66,31 +67,44 @@
6667
}
6768
}
6869

70+
// Size variants
71+
&--size-md {
72+
@include apply-font(label-lg);
73+
74+
:global(.ink-button__icon) {
75+
@include apply-icon(md);
76+
}
77+
}
78+
79+
&--size-sm {
80+
@include apply-font(label-md);
81+
82+
:global(.ink-button__icon) {
83+
@include apply-icon(sm);
84+
}
85+
}
86+
6987
// Type variants
70-
&--default {
71-
&.ink-button--md {
88+
&--type-default {
89+
&.ink-button--size-md {
7290
padding: sys-var(space, sm) sys-var(space, md);
73-
@include apply-font(label-lg);
7491
}
7592

76-
&.ink-button--sm {
93+
&.ink-button--size-sm {
7794
padding: sys-var(space, xs) sys-var(space, md);
78-
@include apply-font(label-md);
7995
}
8096
}
8197

82-
&--icon {
98+
&--type-square {
8399
padding: 0;
84100

85-
&.ink-button--sm {
101+
&.ink-button--size-sm {
86102
height: sys-var(size, sm);
87103
width: sys-var(size, sm);
88-
@include apply-icon(sm);
89104
}
90-
&.ink-button--md {
105+
&.ink-button--size-md {
91106
height: sys-var(size, md);
92107
width: sys-var(size, md);
93-
@include apply-icon(md);
94108
}
95109
}
96110
}

packages/web-design/src/components/inkButton/inkButton.story.md

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ Use it for any single-click action that requires a compact button UI; avoid usin
1212

1313
### Concepts
1414

15-
- `type`: visual and purposeful variant of the button (`subtle`, `primary`, `danger`).
15+
- `theme`: visual and purposeful variant of the button (`subtle`, `primary`, `danger`).
16+
- `type`: shape variant of the button (`default`, `square`).
1617
- `size`: density of the control (`md`, `sm`).
18+
- `icon`: iconfont class name (e.g., `i-mdi-cog`).
19+
- `iconPlacement`: where to place the icon (`prefix`, `suffix`).
20+
- `isLoading`: whether the button is in a loading state.
1721
- `slot`: custom content (e.g., icon + label, label only).
1822

1923
### Visual / UX Meaning
@@ -22,6 +26,7 @@ Use it for any single-click action that requires a compact button UI; avoid usin
2226
- `primary`: main action in a section or modal; higher contrast to attract attention.
2327
- `danger`: destructive action that explains a potentially destructive outcome (e.g., delete).
2428
- `sm`: smaller footprint for compact toolbars; `md`: default size used in forms and actions.
29+
- `square`: used for icon-only buttons.
2530

2631
## Canonical Examples
2732

@@ -49,39 +54,50 @@ Use it for any single-click action that requires a compact button UI; avoid usin
4954
<InkButton text="Edit" size="sm" />
5055
```
5156

57+
- Icon: Use built-in icon prop.
58+
59+
```vue
60+
<InkButton text="Settings" icon="i-mdi-cog" />
61+
```
62+
63+
- Loading: Show loading state.
64+
65+
```vue
66+
<InkButton text="Saving..." :isLoading="true" />
67+
```
68+
5269
### Custom slot content (icon + label)
5370

5471
The `slot` is supported for custom content; this is how you add icons or non-text children.
5572

5673
```vue
5774
<InkButton theme="primary">
58-
<svg aria-hidden="true" width="16" height="16">...</svg>
75+
<div class="i-mdi-content-save" />
5976
<span>Save</span>
60-
</InkButton>
77+
</InkButton>
6178
```
6279

6380
## Behavioral Contract
6481

6582
- In all variants, clicking the button emits a `click` event.
66-
- Click events are always emitted; the component does not internally debounce, block, or prevent repeated clicks.
67-
- The default values are: `text` = "Button Text", `type` = `subtle`, `size` = `md`.
68-
- The component uses a native `<button>` element, so it inherits browser keyboard accessibility (space/enter) and form behavior; consumers should be aware of the native `type` default behavior in forms and add an explicit `type` attribute where necessary to avoid accidental form submits.
83+
- Click events are NOT emitted when `isLoading` is true.
84+
- The default values are: `text` = "Button Text", `theme` = `subtle`, `type` = `default`, `size` = `md`.
85+
- The component uses a native `<button>` element, so it inherits browser keyboard accessibility (space/enter) and form behavior.
6986

7087
## Extension & Composition
7188

7289
- The component is intentionally simple and is designed for composition via the `slot` and surrounding layout.
73-
- Accepts custom slot content for icons or complex inline labels (icon + text). The component does not provide a built-in `icon` prop; use the slot to place an icon element before or after the label.
74-
- Works well inside `FormItem` or other containers. Because it is a native `button`, it behaves like a regular HTML button in forms.
90+
- Accepts custom slot content for icons or complex inline labels (icon + text).
91+
- Works well inside `FormItem` or other containers.
7592

7693
## Non-Goals
7794

78-
- There is no built-in `disabled` or `loading` prop. Disabling or preventing repeated action should be handled by parent components or by a wrapper that manages state.
7995
- It does not provide variant-level keyboard shortcuts or automatic confirmation dialogs.
80-
- It does not manage long-running workflows, API states, or request lifecycle (for example, showing an internal spinner when an action is in-flight).
96+
- It does not manage long-running workflows, API states, or request lifecycle (it only provides the `isLoading` visual state).
8197

8298
## Implementation Notes
8399

84-
- Props: `text` (string, default "Button Text"), `type` (`subtle` | `primary` | `danger`, default `subtle`), `size` (`md` | `sm`, default `md`).
100+
- Props: `text` (string, default "Button Text"), `theme` (`subtle` | `primary` | `danger`, default `subtle`), `type` (`default` | `square`, default `default`), `size` (`md` | `sm`, default `md`), `icon` (string), `iconPlacement` (`prefix` | `suffix`, default `prefix`), `isLoading` (boolean, default `false`).
85101
- Emits: `click` (always true), implemented as a regular native `<button>` click event via `emit('click')`.
86102
- The `slot` is present and should be considered the source of truth for custom content; the `text` prop is a convenience when only a label is used.
87103
- Accessibility note for maintainers: the component omits an explicit `type` attribute on the underlying `<button>`, so by default in HTML forms the button will behave as `submit`. When used inside a form where submission is not intended, the caller should pass a `type="button"` attribute to avoid accidental form submits.

packages/web-design/src/components/inkButton/inkButton.story.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,23 @@ import InkButton from "./inkButton.vue";
5050
<Variant title="Short Text">
5151
<InkButton text="OK" theme="primary" />
5252
</Variant>
53+
54+
<!-- [Visual] Icons -->
55+
<Variant title="Icon Prefix">
56+
<InkButton text="Settings" icon="i-mdi-cog" theme="primary" />
57+
</Variant>
58+
59+
<Variant title="Icon Suffix">
60+
<InkButton
61+
text="Next"
62+
icon="i-mdi-arrow-right"
63+
icon-placement="suffix"
64+
theme="primary"
65+
/>
66+
</Variant>
67+
68+
<Variant title="Icon Only (Square)">
69+
<InkButton icon="i-mdi-plus" type="square" theme="primary" />
70+
</Variant>
5371
</Story>
5472
</template>

packages/web-design/src/components/inkButton/inkButton.test.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,20 @@ describe("InkButton", () => {
1515
expect(wrapper.text()).toContain("Click me");
1616
});
1717

18-
it("applies correct CSS classes for type and size", () => {
18+
it("applies correct CSS classes for theme, type and size", () => {
1919
const wrapper = mount(InkButton, {
2020
props: {
2121
text: "Test",
22-
type: "danger",
22+
theme: "danger",
23+
type: "square",
2324
size: "sm",
2425
},
2526
});
2627

2728
const button = wrapper.find("button");
28-
expect(button.classes()).toContain("ink-button--danger");
29-
expect(button.classes()).toContain("ink-button--sm");
29+
expect(button.classes()).toContain("ink-button--theme-danger");
30+
expect(button.classes()).toContain("ink-button--type-square");
31+
expect(button.classes()).toContain("ink-button--size-sm");
3032
});
3133

3234
it("emits click event when clicked", async () => {
@@ -40,11 +42,37 @@ describe("InkButton", () => {
4042
expect(wrapper.emitted()).toHaveProperty("click");
4143
});
4244

45+
it("does not emit click event when loading", async () => {
46+
const wrapper = mount(InkButton, {
47+
props: {
48+
text: "Click me",
49+
isLoading: true,
50+
},
51+
});
52+
53+
await wrapper.find("button").trigger("click");
54+
expect(wrapper.emitted()).not.toHaveProperty("click");
55+
expect(wrapper.find("button").attributes()).toHaveProperty("disabled");
56+
});
57+
58+
it("renders icon when provided", () => {
59+
const wrapper = mount(InkButton, {
60+
props: {
61+
text: "Settings",
62+
icon: "i-mdi-cog",
63+
},
64+
});
65+
66+
expect(wrapper.find(".ink-button__icon").exists()).toBe(true);
67+
expect(wrapper.find(".ink-button__icon").classes()).toContain("i-mdi-cog");
68+
});
69+
4370
it("uses default values when props not provided", () => {
4471
const wrapper = mount(InkButton);
4572
const button = wrapper.find("button");
4673

47-
expect(button.classes()).toContain("ink-button--subtle");
48-
expect(button.classes()).toContain("ink-button--md");
74+
expect(button.classes()).toContain("ink-button--theme-subtle");
75+
expect(button.classes()).toContain("ink-button--size-md");
76+
expect(button.classes()).toContain("ink-button--type-default");
4977
});
5078
});

packages/web-design/src/components/inkButton/inkButton.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import { makeStringProp, makeBooleanProp } from "../../utils/vue-props";
22

33
// --- Types ---
44
type ButtonTheme = "subtle" | "primary" | "danger";
5-
type ButtonType = "default" | "icon";
5+
type ButtonType = "default" | "square";
66
type ButtonSize = "md" | "sm";
77

88
// --- Props ---
99
export const inkButtonProps = {
10-
text: makeStringProp("Button Text"),
10+
text: makeStringProp(),
11+
/** iconfont class name, eg. i-mdi-menu */
12+
icon: makeStringProp(),
13+
iconPlacement: makeStringProp<"prefix" | "suffix">("prefix"),
1114
type: makeStringProp<ButtonType>("default"),
1215
theme: makeStringProp<ButtonTheme>("subtle"),
1316
size: makeStringProp<ButtonSize>("md"),

0 commit comments

Comments
 (0)