@keyfunc/keybricks-ds 1.0.2 → 1.0.4
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.
- package/DEPLOY.md +3 -3
- package/INTEGRATION.md +24 -5
- package/_adherence.oxlintrc.json +21 -6
- package/components/_cards/radio.card.html +46 -0
- package/components/_cards/segmented-control.card.html +39 -0
- package/components/_cards/toggles.card.html +44 -55
- package/components/_shared/src-buttons.js +1 -1
- package/components/_shared/src-data.js +1 -1
- package/components/_shared/src-display.js +1 -1
- package/components/_shared/src-feedback.js +1 -1
- package/components/_shared/src-forms.js +1 -1
- package/components/_shared/src-navigation.js +1 -1
- package/components/_shared/src-pipeline.js +1 -1
- package/components/_shared/src-tabs.js +1 -1
- package/components/_shared/view-source.js +9 -4
- package/components/_vue/index.mjs +21 -0
- package/components/buttons/Button.vue +0 -20
- package/components/buttons/ButtonGroup.vue +16 -0
- package/components/data/KeyValue.vue +0 -16
- package/components/data/KeyValueList.vue +12 -0
- package/components/data/ListRow.vue +1 -1
- package/components/display/AreaChart.vue +14 -0
- package/components/display/Avatar.vue +0 -17
- package/components/display/AvatarGroup.vue +13 -0
- package/components/display/Badge.vue +0 -34
- package/components/display/Card.vue +0 -15
- package/components/display/Donut.vue +17 -0
- package/components/display/IconTile.vue +13 -0
- package/components/display/MiniBars.vue +11 -0
- package/components/display/ProgressBar.vue +0 -54
- package/components/display/StatCard.vue +11 -0
- package/components/display/StatusBadge.vue +11 -0
- package/components/feedback/ActivityItem.vue +1 -32
- package/components/feedback/Alert.vue +0 -36
- package/components/feedback/DotActivity.vue +15 -0
- package/components/feedback/EmptyState.vue +12 -0
- package/components/feedback/LogViewer.vue +8 -0
- package/components/feedback/Modal.vue +0 -45
- package/components/feedback/Skeleton.vue +6 -0
- package/components/feedback/Spinner.vue +6 -0
- package/components/feedback/Toast.vue +19 -0
- package/components/forms/Combobox.vue +1 -1
- package/components/forms/DatePicker.vue +1 -1
- package/components/forms/Input.jsx +2 -1
- package/components/forms/Input.vue +2 -24
- package/components/forms/Radio.d.ts +28 -0
- package/components/forms/Radio.jsx +67 -0
- package/components/forms/Radio.vue +17 -8
- package/components/forms/RadioGroup.vue +22 -4
- package/components/forms/Switch.d.ts +32 -24
- package/components/forms/Switch.jsx +87 -35
- package/components/forms/Switch.vue +56 -11
- package/components/forms/Textarea.vue +20 -0
- package/components/forms/forms.prompt.md +4 -1
- package/components/forms/input-base.js +3 -0
- package/components/index.d.ts +1 -0
- package/components/index.js +2 -1
- package/components/navigation/Breadcrumb.vue +0 -22
- package/components/navigation/PageHeading.vue +9 -0
- package/components/navigation/PageToolbar.vue +15 -0
- package/components/navigation/ThemeToggle.vue +18 -0
- package/components/navigation/TopBar.vue +0 -32
- package/components/pipeline/NodeCard.vue +2 -1
- package/components/pipeline/PaletteItem.vue +1 -1
- package/components/pipeline/PalettePanel.jsx +2 -8
- package/components/pipeline/PalettePanel.vue +1 -8
- package/components/pipeline/pipeline-catalog.js +8 -0
- package/components/tabs/PanelTabs.vue +10 -0
- package/components/tabs/PillTabs.vue +10 -0
- package/components/tabs/SegmentedControl.d.ts +10 -3
- package/components/tabs/SegmentedControl.jsx +24 -9
- package/components/tabs/SegmentedControl.vue +19 -7
- package/components/tabs/Tabs.vue +0 -28
- package/components/tabs/tabs.prompt.md +3 -1
- package/package.json +1 -1
- package/tokens/tokens.json +1 -1
package/DEPLOY.md
CHANGED
|
@@ -55,11 +55,11 @@ deploys the documentation site. Watch it under *Build → Pipelines*.
|
|
|
55
55
|
**5. Cut the first release.**
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
git tag v1.0.
|
|
58
|
+
git tag v1.0.4 && git push --tags
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
That tag runs `generated:check` → `version:check` → `npm publish`. Confirm under *Deploy →
|
|
62
|
-
Package Registry*: `@keyfunc/keybricks-ds` 1.0.
|
|
62
|
+
Package Registry*: `@keyfunc/keybricks-ds` 1.0.4.
|
|
63
63
|
|
|
64
64
|
**6. Hand developers two things:** the Pages URL, and the `.npmrc` snippet in §4 below.
|
|
65
65
|
|
|
@@ -156,7 +156,7 @@ GitLab injects `CI_JOB_TOKEN` — use that instead and no secret is stored anywh
|
|
|
156
156
|
**Install — straight from git** (no registry, no publish; works today)
|
|
157
157
|
|
|
158
158
|
```json
|
|
159
|
-
"@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v1.0.
|
|
159
|
+
"@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v1.0.4"
|
|
160
160
|
```
|
|
161
161
|
|
|
162
162
|
Because the package has no build step, a git dependency needs no `prepare` script — npm
|
package/INTEGRATION.md
CHANGED
|
@@ -31,7 +31,7 @@ Not using a registry yet? A git dependency works with no publishing at all — t
|
|
|
31
31
|
has no build step, so npm can use the checkout as-is:
|
|
32
32
|
|
|
33
33
|
```json
|
|
34
|
-
"@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v1.0.
|
|
34
|
+
"@keyfunc/keybricks-ds": "git+ssh://git@gitlab.epm2demo.cn/Felix/keybricks-ds.git#v1.0.4"
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
**Source-only package.** `.jsx` and `.vue` files are published as authored, so your
|
|
@@ -81,6 +81,7 @@ export function LoginForm() {
|
|
|
81
81
|
import Button from '@keyfunc/keybricks-ds/components/buttons/Button.vue'
|
|
82
82
|
import Field from '@keyfunc/keybricks-ds/components/forms/Field.vue'
|
|
83
83
|
import Input from '@keyfunc/keybricks-ds/components/forms/Input.vue'
|
|
84
|
+
import Textarea from '@keyfunc/keybricks-ds/components/forms/Textarea.vue'
|
|
84
85
|
</script>
|
|
85
86
|
|
|
86
87
|
<template>
|
|
@@ -126,6 +127,24 @@ Import path is `@keyfunc/keybricks-ds/components/<group>/<Name>.jsx` (React) or
|
|
|
126
127
|
Every component has a sibling `.d.ts` with its full props contract, and each group has a
|
|
127
128
|
`.prompt.md` with usage notes.
|
|
128
129
|
|
|
130
|
+
**One difference between the two trees.** A `.jsx` file may export several related
|
|
131
|
+
components (`Input.jsx` exports `Input` and `Textarea`; `Badge.jsx` exports `Badge` and
|
|
132
|
+
`IconTile`). Vue single-file components hold exactly one component and `<script setup>`
|
|
133
|
+
cannot export values, so on the Vue side every such secondary is its own file with a default
|
|
134
|
+
export — `Textarea.vue`, `IconTile.vue`, `StatusBadge.vue`, `ButtonGroup.vue`, `StatCard.vue`, `Toast.vue`,
|
|
135
|
+
`Spinner.vue`, `PageHeading.vue`, and so on — always named exactly as the React export.
|
|
136
|
+
Shared non-component values live in plain modules: `forms/input-base.js` (`inputBase`),
|
|
137
|
+
`pipeline/pipeline-catalog.js` (`PIPELINE_CATALOG`), `icons/icon-paths.js`.
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
// React
|
|
141
|
+
import { Input, Textarea } from '@keyfunc/keybricks-ds/components/forms/Input.jsx'
|
|
142
|
+
import { Radio, RadioGroup } from '@keyfunc/keybricks-ds/components/forms/Radio.jsx' // no longer inside Switch.jsx
|
|
143
|
+
// Vue
|
|
144
|
+
import Input from '@keyfunc/keybricks-ds/components/forms/Input.vue'
|
|
145
|
+
import Textarea from '@keyfunc/keybricks-ds/components/forms/Textarea.vue'
|
|
146
|
+
```
|
|
147
|
+
|
|
129
148
|
### `brand/` — brand expression
|
|
130
149
|
| Component | What it's for |
|
|
131
150
|
| --- | --- |
|
|
@@ -150,8 +169,8 @@ Every component has a sibling `.d.ts` with its full props contract, and each gro
|
|
|
150
169
|
| `MultiSelect` | Checkbox dropdown; picks render as removable tags with `+n` overflow. |
|
|
151
170
|
| `SearchInput` | Search field with a leading glyph and a clear button. |
|
|
152
171
|
| `Checkbox` | Two sizes, plus an indeterminate state for parent rows. |
|
|
153
|
-
| `Radio` / `RadioGroup` | Single choice from a
|
|
154
|
-
| `Switch` | Immediate on/off
|
|
172
|
+
| `Radio` / `RadioGroup` | Single choice from 2–5 visible options (past five, use `Select`). `description` adds a muted second line; `row` lays out horizontally; `variant="cards"` renders bordered selectable cards for choices a user must weigh. Arrow keys move the selection. Vue: `v-model`. |
|
|
173
|
+
| `Switch` | Immediate on/off setting (no Save button). Three sizes; `size="lg"` when the knob carries `knobIcon` / `knobText` (one character). `insideOn`/`insideOff` for text in the pill, `labelOn`/`labelOff` for captions beside it (or `label` for a single settings caption). `tone="semantic"` is danger-off → success-on — reserve it for states with real consequence. `beforeChange(next)` returns a promise; the knob spins until it settles. Vue: `v-model:checked`. |
|
|
155
174
|
| `NumberStepper` | Numeric input with − / + buttons and min/max clamping. |
|
|
156
175
|
| `Slider` | Range input with a filled track, drag value bubble and optional min/max labels. |
|
|
157
176
|
| `DatePicker` | Text field plus a month-grid popover. Values are `YYYY-MM-DD` strings. |
|
|
@@ -188,7 +207,7 @@ Every component has a sibling `.d.ts` with its full props contract, and each gro
|
|
|
188
207
|
| `Tabs` | Page-level underline tabs. |
|
|
189
208
|
| `PanelTabs` | Compact tabs inside a detail panel or drawer. |
|
|
190
209
|
| `PillTabs` | Filter/status tabs as pills. |
|
|
191
|
-
| `SegmentedControl` |
|
|
210
|
+
| `SegmentedControl` | Switches views of the same data, applies instantly — not a saved form value (`RadioGroup`) and not page navigation (`Tabs`). Two sizes, `icon` per option, `iconOnly`, per-option or whole-control `disabled`, `fullWidth`. ← → keyboard. |
|
|
192
211
|
| `ViewToggle` | List/grid icon toggle. |
|
|
193
212
|
|
|
194
213
|
### `feedback/` — status, overlays, progress
|
|
@@ -407,7 +426,7 @@ a migration note in the release. Token renames are major because your own code r
|
|
|
407
426
|
Pin a minor range and update deliberately:
|
|
408
427
|
|
|
409
428
|
```json
|
|
410
|
-
"@keyfunc/keybricks-ds": "~1.0.
|
|
429
|
+
"@keyfunc/keybricks-ds": "~1.0.4"
|
|
411
430
|
```
|
|
412
431
|
|
|
413
432
|
Release mechanics, the CI pipeline and the Pages site are documented in `DEPLOY.md`.
|
package/_adherence.oxlintrc.json
CHANGED
|
@@ -361,6 +361,14 @@
|
|
|
361
361
|
"selector": "JSXOpeningElement[name.name='RadarAxis'] > JSXAttribute > JSXIdentifier[name!=/^(?:label|max|key|ref|className|style|children)$/]",
|
|
362
362
|
"message": "<RadarAxis> doesn't accept that prop. Declared props: label, max."
|
|
363
363
|
},
|
|
364
|
+
{
|
|
365
|
+
"selector": "JSXOpeningElement[name.name='Radio'] > JSXAttribute > JSXIdentifier[name!=/^(?:checked|onChange|label|description|size|disabled|style|key|ref|className|style|children)$/]",
|
|
366
|
+
"message": "<Radio> doesn't accept that prop. Declared props: checked, onChange, label, description, size, disabled, style."
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"selector": "JSXOpeningElement[name.name='Radio'] > JSXAttribute[name.name='size'] > Literal[value!=/^(?:sm|md)$/]",
|
|
370
|
+
"message": "<Radio> size must be one of 'sm' | 'md'."
|
|
371
|
+
},
|
|
364
372
|
{
|
|
365
373
|
"selector": "JSXOpeningElement[name.name='Ribbon3DSeries'] > JSXAttribute > JSXIdentifier[name!=/^(?:name|values|color|key|ref|className|style|children)$/]",
|
|
366
374
|
"message": "<Ribbon3DSeries> doesn't accept that prop. Declared props: name, values, color."
|
|
@@ -382,8 +390,8 @@
|
|
|
382
390
|
"message": "<SearchInput> doesn't accept that prop. Declared props: placeholder, bare, value, onChange, disabled, style."
|
|
383
391
|
},
|
|
384
392
|
{
|
|
385
|
-
"selector": "JSXOpeningElement[name.name='SegmentedControl'] > JSXAttribute > JSXIdentifier[name!=/^(?:options|value|onChange|size|style|key|ref|className|style|children)$/]",
|
|
386
|
-
"message": "<SegmentedControl> doesn't accept that prop. Declared props: options, value, onChange, size, style."
|
|
393
|
+
"selector": "JSXOpeningElement[name.name='SegmentedControl'] > JSXAttribute > JSXIdentifier[name!=/^(?:options|value|onChange|size|iconOnly|disabled|fullWidth|style|key|ref|className|style|children)$/]",
|
|
394
|
+
"message": "<SegmentedControl> doesn't accept that prop. Declared props: options, value, onChange, size, iconOnly, disabled, fullWidth, style."
|
|
387
395
|
},
|
|
388
396
|
{
|
|
389
397
|
"selector": "JSXOpeningElement[name.name='SegmentedControl'] > JSXAttribute[name.name='size'] > Literal[value!=/^(?:sm|md)$/]",
|
|
@@ -446,12 +454,16 @@
|
|
|
446
454
|
"message": "<Surface3D> doesn't accept that prop. Declared props: values, width, height, yaw, pitch, zScale, wire, interactive, valueFormat, style."
|
|
447
455
|
},
|
|
448
456
|
{
|
|
449
|
-
"selector": "JSXOpeningElement[name.name='Switch'] > JSXAttribute > JSXIdentifier[name!=/^(?:checked|onChange|
|
|
450
|
-
"message": "<Switch> doesn't accept that prop. Declared props: checked, onChange,
|
|
457
|
+
"selector": "JSXOpeningElement[name.name='Switch'] > JSXAttribute > JSXIdentifier[name!=/^(?:checked|onChange|disabled|size|tone|onColor|offColor|knobIcon|knobText|insideOn|insideOff|inlinePrompt|label|labelOn|labelOff|loading|beforeChange|style|key|ref|className|style|children)$/]",
|
|
458
|
+
"message": "<Switch> doesn't accept that prop. Declared props: checked, onChange, disabled, size, tone, onColor, offColor, knobIcon, knobText, insideOn, insideOff, inlinePrompt, label, labelOn, labelOff, loading, beforeChange, style."
|
|
451
459
|
},
|
|
452
460
|
{
|
|
453
|
-
"selector": "JSXOpeningElement[name.name='Switch'] > JSXAttribute[name.name='size'] > Literal[value!=/^(?:md|
|
|
454
|
-
"message": "<Switch> size must be one of 'md' | '
|
|
461
|
+
"selector": "JSXOpeningElement[name.name='Switch'] > JSXAttribute[name.name='size'] > Literal[value!=/^(?:sm|md|lg)$/]",
|
|
462
|
+
"message": "<Switch> size must be one of 'sm' | 'md' | 'lg'."
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
"selector": "JSXOpeningElement[name.name='Switch'] > JSXAttribute[name.name='tone'] > Literal[value!=/^(?:primary|semantic)$/]",
|
|
466
|
+
"message": "<Switch> tone must be one of 'primary' | 'semantic'."
|
|
455
467
|
},
|
|
456
468
|
{
|
|
457
469
|
"selector": "JSXOpeningElement[name.name='Tabs'] > JSXAttribute > JSXIdentifier[name!=/^(?:tabs|active|onChange|style|key|ref|className|style|children)$/]",
|
|
@@ -708,6 +720,9 @@
|
|
|
708
720
|
"RadarAxis": {
|
|
709
721
|
"replaces": []
|
|
710
722
|
},
|
|
723
|
+
"Radio": {
|
|
724
|
+
"replaces": []
|
|
725
|
+
},
|
|
711
726
|
"Ribbon3DSeries": {
|
|
712
727
|
"replaces": []
|
|
713
728
|
},
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x620" name="Radio Button" subtitle="Single choice from a small set — md/sm · description · disabled · row · card variant · arrow-key navigation" -->
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css">
|
|
4
|
+
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
5
|
+
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
|
6
|
+
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
|
7
|
+
<script src="../../_ds_bundle.js"></script>
|
|
8
|
+
<script src="../_shared/view-source.js"></script>
|
|
9
|
+
<style>body{margin:0;padding:20px 24px;font-family:var(--font-sans);background:var(--bg-surface)}.sec{font-size:var(--text-2xs);font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--text-muted);padding:18px 0 6px;border-top:1px solid var(--border-subtle);margin-top:14px}.sec:first-child{border-top:none;margin-top:0;padding-top:0}.case{display:grid;grid-template-columns:170px 1fr;align-items:start;gap:20px;padding:6px 0}.k{font-size:var(--text-xs);color:var(--text-secondary);font-family:var(--font-mono);padding-top:2px}.v{display:flex;align-items:flex-start;gap:24px;flex-wrap:wrap}.note{font-size:var(--text-xs);line-height:17px;color:var(--text-secondary);max-width:60ch;padding-top:6px}a{color:var(--link)}a:hover{color:var(--link-hover)}</style>
|
|
10
|
+
</head><body><div id="root"></div>
|
|
11
|
+
<script type="text/babel">
|
|
12
|
+
const { Radio, RadioGroup } = window.KeybricksDesignSystem_71231a;
|
|
13
|
+
const Case = ({ k, children }) => <div className="case"><span className="k">{k}</span><div className="v">{children}</div></div>;
|
|
14
|
+
function Sheet() {
|
|
15
|
+
const [a, setA] = React.useState("Overwrite"), [b, setB] = React.useState("Ascending"), [c, setC] = React.useState("weekly"), [d, setD] = React.useState("merge"), [e, setE] = React.useState("USD"), [s1, setS1] = React.useState(true), [s2, setS2] = React.useState(true), [s3, setS3] = React.useState(false);
|
|
16
|
+
return <div>
|
|
17
|
+
<div className="sec">Single radio</div>
|
|
18
|
+
<Case k="md · sm"><Radio checked={s1} onChange={() => setS1(true)} label="Selected" /><Radio checked={!s1} onChange={() => setS1(false)} label="Unselected" /><Radio size="sm" checked={s2} onChange={() => setS2(true)} label="Small" /><Radio size="sm" checked={!s2} onChange={() => setS2(false)} label="Small" /></Case>
|
|
19
|
+
<Case k="disabled"><Radio checked disabled label="Locked on" /><Radio disabled label="Locked off" /></Case>
|
|
20
|
+
<Case k="description"><Radio checked={!s3} onChange={() => setS3(false)} label="Weekly digest" description="One summary every Monday morning" /><Radio checked={s3} onChange={() => setS3(true)} label="Daily" description="Every morning at 08:00" /></Case>
|
|
21
|
+
|
|
22
|
+
<div className="sec">RadioGroup — vertical (default)</div>
|
|
23
|
+
<Case k='options: string[]'><RadioGroup options={["New", "Overwrite", "Skip"]} value={a} onChange={setA} /></Case>
|
|
24
|
+
<Case k="with descriptions"><RadioGroup value={c} onChange={setC} options={[
|
|
25
|
+
{ label: "Weekly digest", value: "weekly", description: "One summary every Monday" },
|
|
26
|
+
{ label: "Daily", value: "daily", description: "Every morning at 08:00" },
|
|
27
|
+
{ label: "Never", value: "never", description: "Only in-app notifications", disabled: true }
|
|
28
|
+
]} /></Case>
|
|
29
|
+
|
|
30
|
+
<div className="sec">RadioGroup — row · sm</div>
|
|
31
|
+
<Case k="row"><RadioGroup row options={["USD", "EUR", "CNY", "JPY"]} value={e} onChange={setE} /></Case>
|
|
32
|
+
<Case k='row · size="sm"'><RadioGroup row size="sm" options={["Ascending", "Descending"]} value={b} onChange={setB} /></Case>
|
|
33
|
+
<Case k="disabled group"><RadioGroup row disabled options={["Draft", "Published"]} value="Draft" /></Case>
|
|
34
|
+
|
|
35
|
+
<div className="sec">RadioGroup — card variant</div>
|
|
36
|
+
<Case k='variant="cards" row'><RadioGroup variant="cards" row value={d} onChange={setD} style={{ width: 460 }} options={[
|
|
37
|
+
{ label: "Merge", value: "merge", description: "Keep target values, add new keys" },
|
|
38
|
+
{ label: "Overwrite", value: "overwrite", description: "Target takes source values" },
|
|
39
|
+
{ label: "Skip", value: "skip", description: "Leave conflicts untouched", disabled: true }
|
|
40
|
+
]} /></Case>
|
|
41
|
+
<div className="note">A radio group is the right control for 2–5 mutually exclusive options that should all stay visible. Past five, use <code>Select</code>. Arrow keys move the selection; Space/Enter selects the focused option. Use the card variant when each choice needs a description a user must weigh — migration strategies, plan tiers — never for simple settings.</div>
|
|
42
|
+
</div>;
|
|
43
|
+
}
|
|
44
|
+
ReactDOM.createRoot(document.getElementById("root")).render(<Sheet/>);
|
|
45
|
+
if (window.KBViewSource) setTimeout(function(){ KBViewSource.mount("#root", [{"name":"Radio.jsx","path":"../forms/Radio.jsx"},{"name":"RadioGroup.vue","path":"../forms/RadioGroup.vue","vueOnly":true}]); }, 0);
|
|
46
|
+
</script></body></html>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x520" name="Segmented Control" subtitle="Mutually exclusive views in one inset track — md/sm · icons · icon-only · disabled option · full width · ViewToggle" -->
|
|
2
|
+
<!DOCTYPE html>
|
|
3
|
+
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css">
|
|
4
|
+
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
5
|
+
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
|
6
|
+
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
|
7
|
+
<script src="../../_ds_bundle.js"></script>
|
|
8
|
+
<script src="../_shared/view-source.js"></script>
|
|
9
|
+
<style>body{margin:0;padding:20px 24px;font-family:var(--font-sans);background:var(--bg-surface)}.sec{font-size:var(--text-2xs);font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--text-muted);padding:18px 0 6px;border-top:1px solid var(--border-subtle);margin-top:14px}.sec:first-child{border-top:none;margin-top:0;padding-top:0}.case{display:grid;grid-template-columns:170px 1fr;align-items:center;gap:20px;min-height:36px;padding:5px 0}.k{font-size:var(--text-xs);color:var(--text-secondary);font-family:var(--font-mono)}.v{display:flex;align-items:center;gap:20px;flex-wrap:wrap}.note{font-size:var(--text-xs);line-height:17px;color:var(--text-secondary);max-width:60ch;padding-top:6px}a{color:var(--link)}a:hover{color:var(--link-hover)}</style>
|
|
10
|
+
</head><body><div id="root"></div>
|
|
11
|
+
<script type="text/babel">
|
|
12
|
+
const { SegmentedControl, ViewToggle } = window.KeybricksDesignSystem_71231a;
|
|
13
|
+
const Case = ({ k, children }) => <div className="case"><span className="k">{k}</span><div className="v">{children}</div></div>;
|
|
14
|
+
function Sheet() {
|
|
15
|
+
const [a, setA] = React.useState("Grid"), [b, setB] = React.useState("Last 30 days"), [c, setC] = React.useState("Table"), [d, setD] = React.useState("Actual"), [e, setE] = React.useState("Month"), [f, setF] = React.useState("Overview"), [v, setV] = React.useState("grid");
|
|
16
|
+
return <div>
|
|
17
|
+
<div className="sec">Sizes</div>
|
|
18
|
+
<Case k="md"><SegmentedControl options={["Actual", "Budget", "Variance"]} value={d} onChange={setD} /></Case>
|
|
19
|
+
<Case k="sm"><SegmentedControl size="sm" options={["Last 7 days", "Last 30 days", "Quarter"]} value={b} onChange={setB} /></Case>
|
|
20
|
+
|
|
21
|
+
<div className="sec">With icons</div>
|
|
22
|
+
<Case k="icon + label"><SegmentedControl options={[{ label: "List", icon: "list" }, { label: "Grid", icon: "layout-grid" }]} value={a} onChange={setA} /></Case>
|
|
23
|
+
<Case k="icon only · aria label"><SegmentedControl options={[{ icon: "table", label: "Table" }, { icon: "chart-column", label: "Chart" }, { icon: "code", label: "JSON" }]} iconOnly value={c} onChange={setC} /></Case>
|
|
24
|
+
|
|
25
|
+
<div className="sec">States</div>
|
|
26
|
+
<Case k="disabled option"><SegmentedControl options={[{ label: "Month" }, { label: "Quarter" }, { label: "Year", disabled: true }]} value={e} onChange={setE} /></Case>
|
|
27
|
+
<Case k="disabled control"><SegmentedControl disabled options={["Actual", "Budget"]} value="Actual" /></Case>
|
|
28
|
+
|
|
29
|
+
<div className="sec">Layout</div>
|
|
30
|
+
<Case k="fullWidth"><SegmentedControl fullWidth options={["Overview", "Details", "History"]} value={f} onChange={setF} style={{ width: 400 }} /></Case>
|
|
31
|
+
|
|
32
|
+
<div className="sec">ViewToggle — the two-state shorthand</div>
|
|
33
|
+
<Case k="list / grid"><ViewToggle value={v} onChange={setV} /></Case>
|
|
34
|
+
<div className="note">Segmented control switches <em>views of the same data</em> and applies instantly — never for a form value that gets saved (that's <code>RadioGroup</code>) and never for navigation between pages (that's <code>Tabs</code>). 2–5 options, labels one or two words. <code>ViewToggle</code> is the fixed list/grid pair used in panel headers.</div>
|
|
35
|
+
</div>;
|
|
36
|
+
}
|
|
37
|
+
ReactDOM.createRoot(document.getElementById("root")).render(<Sheet/>);
|
|
38
|
+
if (window.KBViewSource) setTimeout(function(){ KBViewSource.mount("#root", [{"name":"SegmentedControl.jsx","path":"../tabs/SegmentedControl.jsx"},{"name":"ViewToggle.jsx","path":"../tabs/ViewToggle.jsx"}]); }, 0);
|
|
39
|
+
</script></body></html>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- @dsCard group="Components" viewport="
|
|
1
|
+
<!-- @dsCard group="Components" viewport="700x1060" name="Toggle Switch" subtitle="Binary settings pill — sizes · semantic red→green · glyph knobs · inline text · captions · async loading" -->
|
|
2
2
|
<!DOCTYPE html>
|
|
3
3
|
<html><head><meta charset="utf-8"><script src="../_shared/frame-theme.js"></script><link rel="stylesheet" href="../../styles.css">
|
|
4
4
|
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
|
@@ -6,63 +6,52 @@
|
|
|
6
6
|
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
|
7
7
|
<script src="../../_ds_bundle.js"></script>
|
|
8
8
|
<script src="../_shared/view-source.js"></script>
|
|
9
|
-
<style>body{margin:0;
|
|
10
|
-
|
|
9
|
+
<style>body{margin:0;padding:20px 24px;font-family:var(--font-sans);background:var(--bg-surface)}.sec{font-size:var(--text-2xs);font-weight:600;letter-spacing:.7px;text-transform:uppercase;color:var(--text-muted);padding:18px 0 6px;border-top:1px solid var(--border-subtle);margin-top:14px}.sec:first-child{border-top:none;margin-top:0;padding-top:0}.case{display:grid;grid-template-columns:230px 1fr;align-items:center;gap:20px;min-height:36px;padding:5px 0}.k{font-size:var(--text-xs);color:var(--text-secondary);font-family:var(--font-mono)}.v{display:flex;align-items:center;gap:16px;flex-wrap:wrap}.cap{font-size:var(--text-xs);color:var(--text-muted)}a{color:var(--link)}a:hover{color:var(--link-hover)}</style>
|
|
10
|
+
</head><body><div id="root"></div>
|
|
11
11
|
<script type="text/babel">
|
|
12
|
-
const { Switch
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const [
|
|
17
|
-
const [s,
|
|
18
|
-
const [t,setT]=React.useState("Last 30 days");
|
|
19
|
-
const [v,setV]=React.useState("grid");
|
|
12
|
+
const { Switch } = window.KeybricksDesignSystem_71231a;
|
|
13
|
+
const wait = (ms, ok = true) => new Promise(r => setTimeout(() => r(ok), ms));
|
|
14
|
+
const Case = ({ k, children }) => <div className="case"><span className="k">{k}</span><div className="v">{children}</div></div>;
|
|
15
|
+
function Sheet() {
|
|
16
|
+
const [v, set] = React.useState({});
|
|
17
|
+
const b = (k, d = false) => ({ checked: v[k] ?? d, onChange: n => set(s => ({ ...s, [k]: n })) });
|
|
20
18
|
return <div>
|
|
21
|
-
<div
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<div>
|
|
56
|
-
<span className="lbl">SegmentedControl — md / sm · ViewToggle</span>
|
|
57
|
-
<div style={{display:"flex",flexDirection:"column",gap:12,alignItems:"flex-start"}}>
|
|
58
|
-
<SegmentedControl options={[{label:"List",icon:"list"},{label:"Grid",icon:"layout-grid"}]} value={s} onChange={setS}/>
|
|
59
|
-
<SegmentedControl size="sm" options={["Last 7 days","Last 30 days","Quarter"]} value={t} onChange={setT}/>
|
|
60
|
-
<ViewToggle value={v} onChange={setV}/>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
19
|
+
<div className="sec">Sizes & states — sm · md · lg</div>
|
|
20
|
+
<Case k="on"><Switch size="sm" {...b("s1", true)} /><Switch {...b("s2", true)} /><Switch size="lg" {...b("s3", true)} /></Case>
|
|
21
|
+
<Case k="off"><Switch size="sm" {...b("s4")} /><Switch {...b("s5")} /><Switch size="lg" {...b("s6")} /></Case>
|
|
22
|
+
<Case k="disabled"><Switch size="sm" checked disabled /><Switch checked disabled /><Switch size="lg" checked disabled /></Case>
|
|
23
|
+
<Case k="label (settings row)"><Switch size="sm" label="Compact rows" {...b("s7", true)} /><Switch label="Debug Mode" {...b("s8", true)} /><Switch size="lg" label="Auto-run" {...b("s9", true)} /></Case>
|
|
24
|
+
|
|
25
|
+
<div className="sec">Semantic tone — danger off → success on</div>
|
|
26
|
+
<Case k='tone="semantic" · on'><Switch size="sm" tone="semantic" {...b("t1", true)} /><Switch tone="semantic" {...b("t2", true)} /><Switch size="lg" tone="semantic" {...b("t3", true)} /></Case>
|
|
27
|
+
<Case k="off"><Switch size="sm" tone="semantic" {...b("t4")} /><Switch tone="semantic" {...b("t5")} /><Switch size="lg" tone="semantic" {...b("t6")} /></Case>
|
|
28
|
+
|
|
29
|
+
<div className="sec">Glyph in the knob — md · lg only (sm knob is 12px, too small for a glyph)</div>
|
|
30
|
+
<Case k="knobIcon check"><Switch knobIcon="check" {...b("g1", true)} /><Switch size="lg" knobIcon="check" {...b("g2", true)} /></Case>
|
|
31
|
+
<Case k="knobIcon {check, x}"><Switch knobIcon={{ on: "check", off: "x" }} {...b("g3")} /><Switch size="lg" knobIcon={{ on: "check", off: "x" }} {...b("g4")} /></Case>
|
|
32
|
+
<Case k="knobIcon {sun, moon}"><Switch knobIcon={{ on: "sun", off: "moon" }} {...b("g5", true)} /><Switch size="lg" knobIcon={{ on: "sun", off: "moon" }} {...b("g6", true)} /></Case>
|
|
33
|
+
<Case k="knobText {Y, N}"><Switch knobText={{ on: "Y", off: "N" }} {...b("g7", true)} /><Switch size="lg" knobText={{ on: "Y", off: "N" }} {...b("g8", true)} /></Case>
|
|
34
|
+
<Case k="knobText {开, 关}"><Switch knobText={{ on: "开", off: "关" }} {...b("g9")} /><Switch size="lg" knobText={{ on: "开", off: "关" }} {...b("g10")} /></Case>
|
|
35
|
+
<Case k="semantic + knobIcon"><Switch tone="semantic" knobIcon={{ on: "check", off: "x" }} {...b("g11", true)} /><Switch size="lg" tone="semantic" knobIcon={{ on: "check", off: "x" }} {...b("g12", true)} /></Case>
|
|
36
|
+
|
|
37
|
+
<div className="sec">Text inside the pill — md · lg only</div>
|
|
38
|
+
<Case k="ON / OFF"><Switch insideOn="ON" insideOff="OFF" {...b("i1", true)} /><Switch size="lg" insideOn="ON" insideOff="OFF" {...b("i2", true)} /></Case>
|
|
39
|
+
<Case k="开 / 关"><Switch insideOn="开" insideOff="关" {...b("i3")} /><Switch size="lg" insideOn="开" insideOff="关" {...b("i4")} /></Case>
|
|
40
|
+
<Case k="semantic · Y / N"><Switch tone="semantic" insideOn="Y" insideOff="N" {...b("i5", true)} /><Switch size="lg" tone="semantic" insideOn="Y" insideOff="N" {...b("i6", true)} /></Case>
|
|
41
|
+
|
|
42
|
+
<div className="sec">Captions beside — sm · md · lg (active side reads heading)</div>
|
|
43
|
+
<Case k="labelOn"><Switch size="sm" labelOn="Enabled" {...b("l1", true)} /><Switch labelOn="Enabled" {...b("l2", true)} /><Switch size="lg" labelOn="Enabled" {...b("l3", true)} /></Case>
|
|
44
|
+
<Case k="labelOff + labelOn"><Switch size="sm" labelOff="Monthly" labelOn="Annual" {...b("l4")} /><Switch labelOff="Monthly" labelOn="Annual" {...b("l5")} /><Switch size="lg" labelOff="Monthly" labelOn="Annual" {...b("l6")} /></Case>
|
|
45
|
+
<Case k="semantic · Off / On"><Switch size="sm" tone="semantic" labelOff="Off" labelOn="On" {...b("l7", true)} /><Switch tone="semantic" labelOff="Off" labelOn="On" {...b("l8", true)} /><Switch size="lg" tone="semantic" labelOff="Off" labelOn="On" {...b("l9", true)} /></Case>
|
|
46
|
+
|
|
47
|
+
<div className="sec">Loading — sm · md · lg</div>
|
|
48
|
+
<Case k="loading · on"><Switch size="sm" checked loading /><Switch checked loading /><Switch size="lg" checked loading /></Case>
|
|
49
|
+
<Case k="loading · off"><Switch size="sm" loading /><Switch loading /><Switch size="lg" loading /></Case>
|
|
50
|
+
<Case k="semantic · loading"><Switch size="sm" tone="semantic" checked loading /><Switch tone="semantic" checked loading /><Switch size="lg" tone="semantic" checked loading /></Case>
|
|
51
|
+
<Case k="beforeChange → resolves 1.2s"><Switch size="sm" beforeChange={() => wait(1200)} {...b("a1", true)} /><Switch beforeChange={() => wait(1200)} {...b("a2", true)} /><Switch size="lg" knobIcon={{ on: "check", off: "x" }} beforeChange={() => wait(1200)} {...b("a3", true)} /></Case>
|
|
52
|
+
<Case k="beforeChange → rejects"><Switch size="sm" tone="semantic" beforeChange={() => wait(1200, false)} {...b("a4")} /><Switch tone="semantic" beforeChange={() => wait(1200, false)} {...b("a5")} /><Switch size="lg" tone="semantic" knobIcon={{ on: "check", off: "x" }} beforeChange={() => wait(1200, false)} {...b("a6")} /><span className="cap">stays put after the spinner</span></Case>
|
|
64
53
|
</div>;
|
|
65
54
|
}
|
|
66
55
|
ReactDOM.createRoot(document.getElementById("root")).render(<Sheet/>);
|
|
67
|
-
if (window.KBViewSource) setTimeout(function(){ KBViewSource.mount("#root", [{"name":"Switch.jsx","path":"../forms/Switch.jsx"}
|
|
56
|
+
if (window.KBViewSource) setTimeout(function(){ KBViewSource.mount("#root", [{"name":"Switch.jsx","path":"../forms/Switch.jsx"}]); }, 0);
|
|
68
57
|
</script></body></html>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"buttons/ActionChip.jsx":"import React from \"react\";\n\nconst chipTones = {\n success: { background: \"var(--success-bg)\", color: \"var(--success-text)\" },\n warning: { background: \"var(--warning-bg)\", color: \"var(--warning-text)\" },\n neutral: { background: \"var(--bg-hover)\", color: \"var(--text-body)\" },\n info: { background: \"var(--info-bg)\", color: \"var(--action-primary)\" },\n danger: { background: \"var(--danger-bg)\", color: \"var(--danger-text)\" },\n};\n\n/** Tinted bulk-action chip (\"Mark as New\", \"Mark as Overwrite\", \"Skip Selected\"). */\nexport function ActionChip({ tone = \"neutral\", children, onClick, style }) {\n const [hover, setHover] = React.useState(false);\n return (\n <button type=\"button\" onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ height: 30, padding: \"0 12px\", borderRadius: \"var(--radius-md)\", border: \"none\", cursor: \"pointer\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", display: \"inline-flex\", alignItems: \"center\", filter: hover ? \"brightness(1.06) saturate(1.05)\" : \"none\", transition: \"var(--t-hover),filter var(--dur-fast) var(--ease-out)\", ...chipTones[tone], ...style }}>\n {children}\n </button>\n );\n}\n","buttons/ActionChip.vue":"<script setup>\nimport { ref, computed } from 'vue'\nconst chipTones = {\n success: { background: 'var(--success-bg)', color: 'var(--success-text)' },\n warning: { background: 'var(--warning-bg)', color: 'var(--warning-text)' },\n neutral: { background: 'var(--bg-hover)', color: 'var(--text-body)' },\n info: { background: 'var(--info-bg)', color: 'var(--action-primary)' },\n danger: { background: 'var(--danger-bg)', color: 'var(--danger-text)' },\n}\n// Tinted bulk-action chip (\"Mark as New\", \"Mark as Overwrite\", \"Skip Selected\").\nconst props = defineProps({ tone: { type: String, default: 'neutral' }, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\nconst s = computed(() => ({ height: 30, padding: '0 12px', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', display: 'inline-flex', alignItems: 'center', filter: hover.value ? 'brightness(1.06) saturate(1.05)' : 'none', transition: 'var(--t-hover),filter var(--dur-fast) var(--ease-out)', ...chipTones[props.tone], ...props.style }))\n</script>\n<template>\n <button type=\"button\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\" :style=\"s\"><slot /></button>\n</template>\n","buttons/Button.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst btnBase = { display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-regular)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", border: \"1px solid transparent\", cursor: \"pointer\", whiteSpace: \"nowrap\", boxSizing: \"border-box\", background: \"transparent\", transition: \"var(--t-hover)\" };\n\nconst variants = {\n primary: { background: \"var(--action-primary)\", color: \"var(--on-accent)\" },\n dark: { background: \"var(--action-dark)\", color: \"var(--action-dark-fg)\" },\n outline: { background: \"var(--bg-surface)\", color: \"var(--text-body)\", borderColor: \"var(--border-control)\" },\n ghost: { color: \"var(--text-body)\" },\n \"primary-soft\": { background: \"var(--soft-bg)\", color: \"var(--action-primary)\", borderColor: \"var(--soft-border)\" },\n danger: { background: \"var(--danger)\", color: \"var(--on-accent)\" },\n \"danger-soft\": { background: \"var(--danger-bg)\", color: \"var(--danger-text)\" },\n text: { color: \"var(--text-secondary)\", padding: \"0\", height: \"auto\" },\n};\nconst hovers = {\n primary: \"var(--action-primary-hover)\", dark: \"var(--action-dark-hover)\",\n outline: \"var(--bg-hover)\", ghost: \"var(--bg-hover)\", \"primary-soft\": \"var(--soft-hover)\",\n danger: \"var(--danger-text)\", \"danger-soft\": \"var(--danger-bg)\",\n};\n/** Press (mouse-down) fills — one step past hover. */\nconst presses = {\n primary: \"var(--action-primary-active)\", dark: \"var(--action-dark-active)\",\n outline: \"var(--bg-press)\", ghost: \"var(--bg-press)\", \"primary-soft\": \"var(--soft-active)\",\n danger: \"var(--danger-active)\", \"danger-soft\": \"var(--danger-soft-active)\",\n};\n/** Variants that change text color (+ underline) on hover instead of a background wash. */\nconst colorHoverVariants = { text: \"var(--text-heading)\" };\nconst sizes = {\n sm: { height: 30, padding: \"6px 12px\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", letterSpacing: 0, borderRadius: \"var(--radius-md)\" },\n md: { height: 32, padding: \"6px 12px\", borderRadius: \"var(--radius-md)\" },\n lg: { height: 35.5, padding: \"8px 16px\", fontWeight: \"var(--weight-medium)\", borderRadius: \"var(--radius-lg)\" },\n};\nconst iconOnlyPad = { sm: 0, md: 0, lg: 0 };\nconst iconOnlyBox = { sm: 30, md: 32, lg: 36 };\n\nfunction Spinner({ size, color }) {\n return <span style={{ display: \"inline-block\", width: size, height: size, borderRadius: \"var(--radius-full)\", border: \"2px solid currentColor\", borderTopColor: \"transparent\", opacity: .9, color, animation: \"kb-spin .7s linear infinite\", flexShrink: 0 }} />;\n}\n\n/** Keybricks button. variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm | md | lg. Interaction states: hover (one step darker) · press (two steps, set via mouse-down) · keyboard focus ring · loading · disabled. */\nexport function Button({ variant = \"primary\", size = \"md\", icon, iconSize, iconPosition = \"left\", medium, disabled, loading, fullWidth, iconOnly, label, pressed, focused, children, style, onClick, ...rest }) {\n const [hover, setHover] = React.useState(false);\n const [press, setPress] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const busy = disabled || loading;\n const isPressed = press || pressed;\n const isFocused = focus || focused;\n const s = {\n ...btnBase, ...sizes[size], ...variants[variant],\n ...(medium ? { fontWeight: \"var(--weight-medium)\" } : null),\n ...(fullWidth ? { width: \"100%\" } : null),\n ...(iconOnly ? { width: iconOnlyBox[size], padding: iconOnlyPad[size], gap: 0 } : null),\n ...(busy ? { opacity: .55, cursor: \"default\" } : null),\n ...style,\n };\n if (!busy) {\n if (colorHoverVariants[variant]) {\n if (hover || isPressed) { s.color = colorHoverVariants[variant]; s.textDecoration = \"underline\"; }\n if (isPressed) s.opacity = .7;\n } else if (isPressed) {\n s.background = presses[variant];\n } else if (hover) {\n s.background = hovers[variant];\n }\n if (isFocused) s.boxShadow = \"var(--ring-focus)\";\n }\n const iSize = iconSize || (size === \"sm\" ? 12 : 14);\n const leading = loading ? <Spinner size={iSize} color={s.color} /> : (icon && iconPosition === \"left\" ? <Icon name={icon} size={iSize} /> : null);\n const trailing = !loading && icon && iconPosition === \"right\" ? <Icon name={icon} size={iSize} /> : null;\n return (\n <button type=\"button\" data-press=\"own\" style={s} disabled={busy} aria-label={iconOnly ? label : undefined} onClick={onClick}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}\n onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}\n onKeyDown={(e) => { if (e.key === \" \" || e.key === \"Enter\") setPress(true); }} onKeyUp={() => setPress(false)}\n onFocus={(e) => { try { setFocus(e.target.matches(\":focus-visible\")); } catch (err) { setFocus(true); } }} onBlur={() => setFocus(false)} {...rest}>\n {leading}\n {iconOnly ? null : children}\n {trailing}\n </button>\n );\n}\n\n/** Attached row of Buttons sharing one outline (segmented action group). Children should be <Button variant=\"outline\"> or similar. */\nexport function ButtonGroup({ children, style }) {\n const items = React.Children.toArray(children);\n return (\n <div style={{ display: \"inline-flex\", ...style }}>\n {items.map((child, i) => React.cloneElement(child, {\n key: i,\n style: {\n ...(child.props.style || {}),\n borderRadius: 0,\n ...(i === 0 ? { borderTopLeftRadius: \"var(--radius-md)\", borderBottomLeftRadius: \"var(--radius-md)\" } : null),\n ...(i === items.length - 1 ? { borderTopRightRadius: \"var(--radius-md)\", borderBottomRightRadius: \"var(--radius-md)\" } : null),\n marginLeft: i === 0 ? 0 : -1,\n },\n }))}\n </div>\n );\n}\n","buttons/Button.vue":"<script setup>\nimport { computed, ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n\nconst btnBase = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-regular)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', border: '1px solid transparent', cursor: 'pointer', whiteSpace: 'nowrap', boxSizing: 'border-box', background: 'transparent', transition: 'var(--t-hover)' }\nconst variants = {\n primary: { background: 'var(--action-primary)', color: 'var(--on-accent)' },\n dark: { background: 'var(--action-dark)', color: 'var(--action-dark-fg)' },\n outline: { background: 'var(--bg-surface)', color: 'var(--text-body)', borderColor: 'var(--border-control)' },\n ghost: { color: 'var(--text-body)' },\n 'primary-soft': { background: 'var(--soft-bg)', color: 'var(--action-primary)', borderColor: 'var(--soft-border)' },\n danger: { background: 'var(--danger)', color: 'var(--on-accent)' },\n 'danger-soft': { background: 'var(--danger-bg)', color: 'var(--danger-text)' },\n text: { color: 'var(--text-secondary)', padding: '0', height: 'auto' },\n}\nconst hovers = { primary: 'var(--action-primary-hover)', dark: 'var(--action-dark-hover)', outline: 'var(--bg-hover)', ghost: 'var(--bg-hover)', 'primary-soft': 'var(--soft-hover)', danger: 'var(--danger-text)', 'danger-soft': 'var(--danger-bg)' }\nconst presses = { primary: 'var(--action-primary-active)', dark: 'var(--action-dark-active)', outline: 'var(--bg-press)', ghost: 'var(--bg-press)', 'primary-soft': 'var(--soft-active)', danger: 'var(--danger-active)', 'danger-soft': 'var(--danger-soft-active)' }\nconst colorHoverVariants = { text: 'var(--text-heading)' }\nconst sizes = {\n sm: { height: 30, padding: '6px 12px', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', letterSpacing: 0, borderRadius: 'var(--radius-md)' },\n md: { height: 32, padding: '6px 12px', borderRadius: 'var(--radius-md)' },\n lg: { height: 35.5, padding: '8px 16px', fontWeight: 'var(--weight-medium)', borderRadius: 'var(--radius-lg)' },\n}\nconst iconOnlyPad = { sm: 0, md: 0, lg: 0 }\nconst iconOnlyBox = { sm: 30, md: 32, lg: 36 }\n\n// Button — variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm | md | lg.\nconst props = defineProps({\n variant: { type: String, default: 'primary' }, size: { type: String, default: 'md' },\n icon: String, iconSize: Number, iconPosition: { type: String, default: 'left' },\n medium: Boolean, disabled: Boolean, loading: Boolean, fullWidth: Boolean, iconOnly: Boolean,\n label: String, pressed: Boolean, focused: Boolean, style: Object,\n})\ndefineEmits(['click'])\nconst hover = ref(false), press = ref(false), focus = ref(false)\nconst busy = computed(() => props.disabled || props.loading)\nconst isPressed = computed(() => press.value || props.pressed)\nconst isFocused = computed(() => focus.value || props.focused)\nconst s = computed(() => {\n const base = { ...btnBase, ...sizes[props.size], ...variants[props.variant],\n ...(props.medium ? { fontWeight: 'var(--weight-medium)' } : null),\n ...(props.fullWidth ? { width: '100%' } : null),\n ...(props.iconOnly ? { width: iconOnlyBox[props.size], padding: iconOnlyPad[props.size], gap: 0 } : null),\n ...(busy.value ? { opacity: .55, cursor: 'default' } : null), ...props.style }\n if (!busy.value) {\n if (colorHoverVariants[props.variant]) {\n if (hover.value || isPressed.value) { base.color = colorHoverVariants[props.variant]; base.textDecoration = 'underline' }\n if (isPressed.value) base.opacity = .7\n } else if (isPressed.value) base.background = presses[props.variant]\n else if (hover.value) base.background = hovers[props.variant]\n if (isFocused.value) base.boxShadow = 'var(--ring-focus)'\n }\n return base\n})\nconst iSize = computed(() => props.iconSize || (props.size === 'sm' ? 12 : 14))\n</script>\n<template>\n <button type=\"button\" data-press=\"own\" :style=\"s\" :disabled=\"busy\" :aria-label=\"iconOnly ? label : undefined\"\n @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false; press = false\"\n @mousedown=\"press = true\" @mouseup=\"press = false\" @keydown.enter=\"press = true\" @keydown.space=\"press = true\" @keyup=\"press = false\"\n @focus=\"focus = $event.target.matches(':focus-visible')\" @blur=\"focus = false\">\n <span v-if=\"loading\" :style=\"{ display: 'inline-block', width: iSize, height: iSize, borderRadius: 'var(--radius-full)', border: '2px solid currentColor', borderTopColor: 'transparent', opacity: .9, animation: 'kb-spin .7s linear infinite', flexShrink: 0 }\" />\n <Icon v-else-if=\"icon && iconPosition === 'left'\" :name=\"icon\" :size=\"iSize\" />\n <slot v-if=\"!iconOnly\" />\n <Icon v-if=\"!loading && icon && iconPosition === 'right'\" :name=\"icon\" :size=\"iSize\" />\n </button>\n</template>\n\n<!-- ButtonGroup — attached row of Buttons sharing one outline (segmented action group). -->\n<!--\n<script setup name=\"ButtonGroup\">\nimport { useSlots, computed } from 'vue'\nconst props = defineProps({ style: Object })\nconst slots = useSlots()\nconst items = computed(() => slots.default ? slots.default() : [])\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', ...style }\">\n <template v-for=\"(child, i) in items\" :key=\"i\">\n <component :is=\"child\" :style=\"{ borderRadius: 0,\n ...(i === 0 ? { borderTopLeftRadius: 'var(--radius-md)', borderBottomLeftRadius: 'var(--radius-md)' } : null),\n ...(i === items.length - 1 ? { borderTopRightRadius: 'var(--radius-md)', borderBottomRightRadius: 'var(--radius-md)' } : null),\n marginLeft: i === 0 ? 0 : -1 }\" />\n </template>\n </div>\n</template>\n-->\n","buttons/IconButton.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst ibSizes = { xs: { box: 22, icon: 14, radius: \"var(--radius-md)\" }, sm: { box: 26, icon: 14, radius: \"var(--radius-sm)\" }, md: { box: 28, icon: 16, radius: \"var(--radius-md)\" }, lg: { box: 34, icon: 18, radius: \"var(--radius-md)\" } };\n\n/** Square ghost icon button (toolbar/back/bell/view toggles). size: xs 22 | sm 26 | md 28 | lg 34. danger tints red; loading shows a spinner and disables interaction. States: hover · press · keyboard focus ring · active (persistent toggle-on) · disabled. */\nexport function IconButton({ icon, size = \"md\", active, muted, danger, disabled, loading, label, pressed, focused, style, onClick, ...rest }) {\n const [hover, setHover] = React.useState(false);\n const [press, setPress] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const t = ibSizes[size];\n const busy = disabled || loading;\n const isPressed = (press || pressed) && !busy;\n const color = danger ? \"var(--danger)\" : muted ? \"var(--text-muted)\" : \"var(--text-body)\";\n const bg = isPressed ? (danger ? \"var(--danger-soft-active)\" : \"var(--bg-press)\")\n : active ? \"var(--bg-active)\"\n : hover && !busy ? (danger ? \"var(--danger-bg)\" : \"var(--bg-hover)\") : \"transparent\";\n return (\n <button type=\"button\" data-press=\"own\" aria-label={label} disabled={busy} onClick={onClick}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}\n onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}\n onKeyDown={(e) => { if (e.key === \" \" || e.key === \"Enter\") setPress(true); }} onKeyUp={() => setPress(false)}\n onFocus={(e) => { try { setFocus(e.target.matches(\":focus-visible\")); } catch (err) { setFocus(true); } }} onBlur={() => setFocus(false)}\n style={{ width: t.box, height: t.box, borderRadius: t.radius, border: \"none\", cursor: busy ? \"default\" : \"pointer\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", background: bg, color, opacity: busy ? .5 : 1, boxShadow: (focus || focused) && !busy ? \"var(--ring-focus)\" : \"none\", transition: \"var(--t-hover)\", padding: 0, flexShrink: 0, ...style }} {...rest}>\n {loading ? <span style={{ display: \"inline-block\", width: t.icon, height: t.icon, borderRadius: \"var(--radius-full)\", border: \"2px solid currentColor\", borderTopColor: \"transparent\", animation: \"kb-spin .7s linear infinite\" }} /> : <Icon name={icon} size={t.icon} />}\n </button>\n );\n}\n","buttons/IconButton.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst ibSizes = { xs: { box: 22, icon: 14, radius: 'var(--radius-md)' }, sm: { box: 26, icon: 14, radius: 'var(--radius-sm)' }, md: { box: 28, icon: 16, radius: 'var(--radius-md)' }, lg: { box: 34, icon: 18, radius: 'var(--radius-md)' } }\n// Square ghost icon button (toolbar/back/bell/view toggles). size: xs 22 | sm 26 | md 28 | lg 34.\nconst props = defineProps({ icon: String, size: { type: String, default: 'md' }, active: Boolean, muted: Boolean, danger: Boolean, disabled: Boolean, loading: Boolean, label: String, pressed: Boolean, focused: Boolean, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false), press = ref(false), focus = ref(false)\nconst t = computed(() => ibSizes[props.size])\nconst busy = computed(() => props.disabled || props.loading)\nconst isPressed = computed(() => (press.value || props.pressed) && !busy.value)\nconst color = computed(() => props.danger ? 'var(--danger)' : props.muted ? 'var(--text-muted)' : 'var(--text-body)')\nconst bg = computed(() => isPressed.value ? (props.danger ? 'var(--danger-soft-active)' : 'var(--bg-press)') : props.active ? 'var(--bg-active)' : (hover.value && !busy.value ? (props.danger ? 'var(--danger-bg)' : 'var(--bg-hover)') : 'transparent'))\nconst s = computed(() => ({ width: t.value.box, height: t.value.box, borderRadius: t.value.radius, border: 'none', cursor: busy.value ? 'default' : 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: bg.value, color: color.value, opacity: busy.value ? .5 : 1, boxShadow: (focus.value || props.focused) && !busy.value ? 'var(--ring-focus)' : 'none', transition: 'var(--t-hover)', padding: 0, flexShrink: 0, ...props.style }))\n</script>\n<template>\n <button type=\"button\" data-press=\"own\" :aria-label=\"label\" :disabled=\"busy\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false; press = false\" @mousedown=\"press = true\" @mouseup=\"press = false\" @keydown.enter=\"press = true\" @keydown.space=\"press = true\" @keyup=\"press = false\" @focus=\"focus = $event.target.matches(':focus-visible')\" @blur=\"focus = false\" :style=\"s\">\n <span v-if=\"loading\" :style=\"{ display: 'inline-block', width: t.icon, height: t.icon, borderRadius: 'var(--radius-full)', border: '2px solid currentColor', borderTopColor: 'transparent', animation: 'kb-spin .7s linear infinite' }\" />\n <Icon v-else :name=\"icon\" :size=\"t.icon\" />\n </button>\n</template>\n"});
|
|
1
|
+
window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"buttons/ActionChip.jsx":"import React from \"react\";\n\nconst chipTones = {\n success: { background: \"var(--success-bg)\", color: \"var(--success-text)\" },\n warning: { background: \"var(--warning-bg)\", color: \"var(--warning-text)\" },\n neutral: { background: \"var(--bg-hover)\", color: \"var(--text-body)\" },\n info: { background: \"var(--info-bg)\", color: \"var(--action-primary)\" },\n danger: { background: \"var(--danger-bg)\", color: \"var(--danger-text)\" },\n};\n\n/** Tinted bulk-action chip (\"Mark as New\", \"Mark as Overwrite\", \"Skip Selected\"). */\nexport function ActionChip({ tone = \"neutral\", children, onClick, style }) {\n const [hover, setHover] = React.useState(false);\n return (\n <button type=\"button\" onClick={onClick} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}\n style={{ height: 30, padding: \"0 12px\", borderRadius: \"var(--radius-md)\", border: \"none\", cursor: \"pointer\", fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-medium)\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", display: \"inline-flex\", alignItems: \"center\", filter: hover ? \"brightness(1.06) saturate(1.05)\" : \"none\", transition: \"var(--t-hover),filter var(--dur-fast) var(--ease-out)\", ...chipTones[tone], ...style }}>\n {children}\n </button>\n );\n}\n","buttons/ActionChip.vue":"<script setup>\nimport { ref, computed } from 'vue'\nconst chipTones = {\n success: { background: 'var(--success-bg)', color: 'var(--success-text)' },\n warning: { background: 'var(--warning-bg)', color: 'var(--warning-text)' },\n neutral: { background: 'var(--bg-hover)', color: 'var(--text-body)' },\n info: { background: 'var(--info-bg)', color: 'var(--action-primary)' },\n danger: { background: 'var(--danger-bg)', color: 'var(--danger-text)' },\n}\n// Tinted bulk-action chip (\"Mark as New\", \"Mark as Overwrite\", \"Skip Selected\").\nconst props = defineProps({ tone: { type: String, default: 'neutral' }, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false)\nconst s = computed(() => ({ height: 30, padding: '0 12px', borderRadius: 'var(--radius-md)', border: 'none', cursor: 'pointer', fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-medium)', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', display: 'inline-flex', alignItems: 'center', filter: hover.value ? 'brightness(1.06) saturate(1.05)' : 'none', transition: 'var(--t-hover),filter var(--dur-fast) var(--ease-out)', ...chipTones[props.tone], ...props.style }))\n</script>\n<template>\n <button type=\"button\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false\" :style=\"s\"><slot /></button>\n</template>\n","buttons/Button.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst btnBase = { display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", gap: 6, fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-regular)\", fontSize: \"var(--text-base)\", lineHeight: \"var(--leading-base)\", letterSpacing: \"var(--tracking-base)\", border: \"1px solid transparent\", cursor: \"pointer\", whiteSpace: \"nowrap\", boxSizing: \"border-box\", background: \"transparent\", transition: \"var(--t-hover)\" };\n\nconst variants = {\n primary: { background: \"var(--action-primary)\", color: \"var(--on-accent)\" },\n dark: { background: \"var(--action-dark)\", color: \"var(--action-dark-fg)\" },\n outline: { background: \"var(--bg-surface)\", color: \"var(--text-body)\", borderColor: \"var(--border-control)\" },\n ghost: { color: \"var(--text-body)\" },\n \"primary-soft\": { background: \"var(--soft-bg)\", color: \"var(--action-primary)\", borderColor: \"var(--soft-border)\" },\n danger: { background: \"var(--danger)\", color: \"var(--on-accent)\" },\n \"danger-soft\": { background: \"var(--danger-bg)\", color: \"var(--danger-text)\" },\n text: { color: \"var(--text-secondary)\", padding: \"0\", height: \"auto\" },\n};\nconst hovers = {\n primary: \"var(--action-primary-hover)\", dark: \"var(--action-dark-hover)\",\n outline: \"var(--bg-hover)\", ghost: \"var(--bg-hover)\", \"primary-soft\": \"var(--soft-hover)\",\n danger: \"var(--danger-text)\", \"danger-soft\": \"var(--danger-bg)\",\n};\n/** Press (mouse-down) fills — one step past hover. */\nconst presses = {\n primary: \"var(--action-primary-active)\", dark: \"var(--action-dark-active)\",\n outline: \"var(--bg-press)\", ghost: \"var(--bg-press)\", \"primary-soft\": \"var(--soft-active)\",\n danger: \"var(--danger-active)\", \"danger-soft\": \"var(--danger-soft-active)\",\n};\n/** Variants that change text color (+ underline) on hover instead of a background wash. */\nconst colorHoverVariants = { text: \"var(--text-heading)\" };\nconst sizes = {\n sm: { height: 30, padding: \"6px 12px\", fontSize: \"var(--text-sm)\", lineHeight: \"var(--leading-sm)\", letterSpacing: 0, borderRadius: \"var(--radius-md)\" },\n md: { height: 32, padding: \"6px 12px\", borderRadius: \"var(--radius-md)\" },\n lg: { height: 35.5, padding: \"8px 16px\", fontWeight: \"var(--weight-medium)\", borderRadius: \"var(--radius-lg)\" },\n};\nconst iconOnlyPad = { sm: 0, md: 0, lg: 0 };\nconst iconOnlyBox = { sm: 30, md: 32, lg: 36 };\n\nfunction Spinner({ size, color }) {\n return <span style={{ display: \"inline-block\", width: size, height: size, borderRadius: \"var(--radius-full)\", border: \"2px solid currentColor\", borderTopColor: \"transparent\", opacity: .9, color, animation: \"kb-spin .7s linear infinite\", flexShrink: 0 }} />;\n}\n\n/** Keybricks button. variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm | md | lg. Interaction states: hover (one step darker) · press (two steps, set via mouse-down) · keyboard focus ring · loading · disabled. */\nexport function Button({ variant = \"primary\", size = \"md\", icon, iconSize, iconPosition = \"left\", medium, disabled, loading, fullWidth, iconOnly, label, pressed, focused, children, style, onClick, ...rest }) {\n const [hover, setHover] = React.useState(false);\n const [press, setPress] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const busy = disabled || loading;\n const isPressed = press || pressed;\n const isFocused = focus || focused;\n const s = {\n ...btnBase, ...sizes[size], ...variants[variant],\n ...(medium ? { fontWeight: \"var(--weight-medium)\" } : null),\n ...(fullWidth ? { width: \"100%\" } : null),\n ...(iconOnly ? { width: iconOnlyBox[size], padding: iconOnlyPad[size], gap: 0 } : null),\n ...(busy ? { opacity: .55, cursor: \"default\" } : null),\n ...style,\n };\n if (!busy) {\n if (colorHoverVariants[variant]) {\n if (hover || isPressed) { s.color = colorHoverVariants[variant]; s.textDecoration = \"underline\"; }\n if (isPressed) s.opacity = .7;\n } else if (isPressed) {\n s.background = presses[variant];\n } else if (hover) {\n s.background = hovers[variant];\n }\n if (isFocused) s.boxShadow = \"var(--ring-focus)\";\n }\n const iSize = iconSize || (size === \"sm\" ? 12 : 14);\n const leading = loading ? <Spinner size={iSize} color={s.color} /> : (icon && iconPosition === \"left\" ? <Icon name={icon} size={iSize} /> : null);\n const trailing = !loading && icon && iconPosition === \"right\" ? <Icon name={icon} size={iSize} /> : null;\n return (\n <button type=\"button\" data-press=\"own\" style={s} disabled={busy} aria-label={iconOnly ? label : undefined} onClick={onClick}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}\n onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}\n onKeyDown={(e) => { if (e.key === \" \" || e.key === \"Enter\") setPress(true); }} onKeyUp={() => setPress(false)}\n onFocus={(e) => { try { setFocus(e.target.matches(\":focus-visible\")); } catch (err) { setFocus(true); } }} onBlur={() => setFocus(false)} {...rest}>\n {leading}\n {iconOnly ? null : children}\n {trailing}\n </button>\n );\n}\n\n/** Attached row of Buttons sharing one outline (segmented action group). Children should be <Button variant=\"outline\"> or similar. */\nexport function ButtonGroup({ children, style }) {\n const items = React.Children.toArray(children);\n return (\n <div style={{ display: \"inline-flex\", ...style }}>\n {items.map((child, i) => React.cloneElement(child, {\n key: i,\n style: {\n ...(child.props.style || {}),\n borderRadius: 0,\n ...(i === 0 ? { borderTopLeftRadius: \"var(--radius-md)\", borderBottomLeftRadius: \"var(--radius-md)\" } : null),\n ...(i === items.length - 1 ? { borderTopRightRadius: \"var(--radius-md)\", borderBottomRightRadius: \"var(--radius-md)\" } : null),\n marginLeft: i === 0 ? 0 : -1,\n },\n }))}\n </div>\n );\n}\n","buttons/Button.vue":"<script setup>\nimport { computed, ref } from 'vue'\nimport Icon from '../icons/Icon.vue'\n\nconst btnBase = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontFamily: 'var(--font-sans)', fontWeight: 'var(--weight-regular)', fontSize: 'var(--text-base)', lineHeight: 'var(--leading-base)', letterSpacing: 'var(--tracking-base)', border: '1px solid transparent', cursor: 'pointer', whiteSpace: 'nowrap', boxSizing: 'border-box', background: 'transparent', transition: 'var(--t-hover)' }\nconst variants = {\n primary: { background: 'var(--action-primary)', color: 'var(--on-accent)' },\n dark: { background: 'var(--action-dark)', color: 'var(--action-dark-fg)' },\n outline: { background: 'var(--bg-surface)', color: 'var(--text-body)', borderColor: 'var(--border-control)' },\n ghost: { color: 'var(--text-body)' },\n 'primary-soft': { background: 'var(--soft-bg)', color: 'var(--action-primary)', borderColor: 'var(--soft-border)' },\n danger: { background: 'var(--danger)', color: 'var(--on-accent)' },\n 'danger-soft': { background: 'var(--danger-bg)', color: 'var(--danger-text)' },\n text: { color: 'var(--text-secondary)', padding: '0', height: 'auto' },\n}\nconst hovers = { primary: 'var(--action-primary-hover)', dark: 'var(--action-dark-hover)', outline: 'var(--bg-hover)', ghost: 'var(--bg-hover)', 'primary-soft': 'var(--soft-hover)', danger: 'var(--danger-text)', 'danger-soft': 'var(--danger-bg)' }\nconst presses = { primary: 'var(--action-primary-active)', dark: 'var(--action-dark-active)', outline: 'var(--bg-press)', ghost: 'var(--bg-press)', 'primary-soft': 'var(--soft-active)', danger: 'var(--danger-active)', 'danger-soft': 'var(--danger-soft-active)' }\nconst colorHoverVariants = { text: 'var(--text-heading)' }\nconst sizes = {\n sm: { height: 30, padding: '6px 12px', fontSize: 'var(--text-sm)', lineHeight: 'var(--leading-sm)', letterSpacing: 0, borderRadius: 'var(--radius-md)' },\n md: { height: 32, padding: '6px 12px', borderRadius: 'var(--radius-md)' },\n lg: { height: 35.5, padding: '8px 16px', fontWeight: 'var(--weight-medium)', borderRadius: 'var(--radius-lg)' },\n}\nconst iconOnlyPad = { sm: 0, md: 0, lg: 0 }\nconst iconOnlyBox = { sm: 30, md: 32, lg: 36 }\n\n// Button — variant: primary | dark | outline | ghost | primary-soft | danger | danger-soft | text; size: sm | md | lg.\nconst props = defineProps({\n variant: { type: String, default: 'primary' }, size: { type: String, default: 'md' },\n icon: String, iconSize: Number, iconPosition: { type: String, default: 'left' },\n medium: Boolean, disabled: Boolean, loading: Boolean, fullWidth: Boolean, iconOnly: Boolean,\n label: String, pressed: Boolean, focused: Boolean, style: Object,\n})\ndefineEmits(['click'])\nconst hover = ref(false), press = ref(false), focus = ref(false)\nconst busy = computed(() => props.disabled || props.loading)\nconst isPressed = computed(() => press.value || props.pressed)\nconst isFocused = computed(() => focus.value || props.focused)\nconst s = computed(() => {\n const base = { ...btnBase, ...sizes[props.size], ...variants[props.variant],\n ...(props.medium ? { fontWeight: 'var(--weight-medium)' } : null),\n ...(props.fullWidth ? { width: '100%' } : null),\n ...(props.iconOnly ? { width: iconOnlyBox[props.size], padding: iconOnlyPad[props.size], gap: 0 } : null),\n ...(busy.value ? { opacity: .55, cursor: 'default' } : null), ...props.style }\n if (!busy.value) {\n if (colorHoverVariants[props.variant]) {\n if (hover.value || isPressed.value) { base.color = colorHoverVariants[props.variant]; base.textDecoration = 'underline' }\n if (isPressed.value) base.opacity = .7\n } else if (isPressed.value) base.background = presses[props.variant]\n else if (hover.value) base.background = hovers[props.variant]\n if (isFocused.value) base.boxShadow = 'var(--ring-focus)'\n }\n return base\n})\nconst iSize = computed(() => props.iconSize || (props.size === 'sm' ? 12 : 14))\n</script>\n<template>\n <button type=\"button\" data-press=\"own\" :style=\"s\" :disabled=\"busy\" :aria-label=\"iconOnly ? label : undefined\"\n @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false; press = false\"\n @mousedown=\"press = true\" @mouseup=\"press = false\" @keydown.enter=\"press = true\" @keydown.space=\"press = true\" @keyup=\"press = false\"\n @focus=\"focus = $event.target.matches(':focus-visible')\" @blur=\"focus = false\">\n <span v-if=\"loading\" :style=\"{ display: 'inline-block', width: iSize, height: iSize, borderRadius: 'var(--radius-full)', border: '2px solid currentColor', borderTopColor: 'transparent', opacity: .9, animation: 'kb-spin .7s linear infinite', flexShrink: 0 }\" />\n <Icon v-else-if=\"icon && iconPosition === 'left'\" :name=\"icon\" :size=\"iSize\" />\n <slot v-if=\"!iconOnly\" />\n <Icon v-if=\"!loading && icon && iconPosition === 'right'\" :name=\"icon\" :size=\"iSize\" />\n </button>\n</template>\n","buttons/IconButton.jsx":"import React from \"react\";\nimport { Icon } from \"../icons/Icon.jsx\";\n\nconst ibSizes = { xs: { box: 22, icon: 14, radius: \"var(--radius-md)\" }, sm: { box: 26, icon: 14, radius: \"var(--radius-sm)\" }, md: { box: 28, icon: 16, radius: \"var(--radius-md)\" }, lg: { box: 34, icon: 18, radius: \"var(--radius-md)\" } };\n\n/** Square ghost icon button (toolbar/back/bell/view toggles). size: xs 22 | sm 26 | md 28 | lg 34. danger tints red; loading shows a spinner and disables interaction. States: hover · press · keyboard focus ring · active (persistent toggle-on) · disabled. */\nexport function IconButton({ icon, size = \"md\", active, muted, danger, disabled, loading, label, pressed, focused, style, onClick, ...rest }) {\n const [hover, setHover] = React.useState(false);\n const [press, setPress] = React.useState(false);\n const [focus, setFocus] = React.useState(false);\n const t = ibSizes[size];\n const busy = disabled || loading;\n const isPressed = (press || pressed) && !busy;\n const color = danger ? \"var(--danger)\" : muted ? \"var(--text-muted)\" : \"var(--text-body)\";\n const bg = isPressed ? (danger ? \"var(--danger-soft-active)\" : \"var(--bg-press)\")\n : active ? \"var(--bg-active)\"\n : hover && !busy ? (danger ? \"var(--danger-bg)\" : \"var(--bg-hover)\") : \"transparent\";\n return (\n <button type=\"button\" data-press=\"own\" aria-label={label} disabled={busy} onClick={onClick}\n onMouseEnter={() => setHover(true)} onMouseLeave={() => { setHover(false); setPress(false); }}\n onMouseDown={() => setPress(true)} onMouseUp={() => setPress(false)}\n onKeyDown={(e) => { if (e.key === \" \" || e.key === \"Enter\") setPress(true); }} onKeyUp={() => setPress(false)}\n onFocus={(e) => { try { setFocus(e.target.matches(\":focus-visible\")); } catch (err) { setFocus(true); } }} onBlur={() => setFocus(false)}\n style={{ width: t.box, height: t.box, borderRadius: t.radius, border: \"none\", cursor: busy ? \"default\" : \"pointer\", display: \"inline-flex\", alignItems: \"center\", justifyContent: \"center\", background: bg, color, opacity: busy ? .5 : 1, boxShadow: (focus || focused) && !busy ? \"var(--ring-focus)\" : \"none\", transition: \"var(--t-hover)\", padding: 0, flexShrink: 0, ...style }} {...rest}>\n {loading ? <span style={{ display: \"inline-block\", width: t.icon, height: t.icon, borderRadius: \"var(--radius-full)\", border: \"2px solid currentColor\", borderTopColor: \"transparent\", animation: \"kb-spin .7s linear infinite\" }} /> : <Icon name={icon} size={t.icon} />}\n </button>\n );\n}\n","buttons/IconButton.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport Icon from '../icons/Icon.vue'\nconst ibSizes = { xs: { box: 22, icon: 14, radius: 'var(--radius-md)' }, sm: { box: 26, icon: 14, radius: 'var(--radius-sm)' }, md: { box: 28, icon: 16, radius: 'var(--radius-md)' }, lg: { box: 34, icon: 18, radius: 'var(--radius-md)' } }\n// Square ghost icon button (toolbar/back/bell/view toggles). size: xs 22 | sm 26 | md 28 | lg 34.\nconst props = defineProps({ icon: String, size: { type: String, default: 'md' }, active: Boolean, muted: Boolean, danger: Boolean, disabled: Boolean, loading: Boolean, label: String, pressed: Boolean, focused: Boolean, style: Object })\ndefineEmits(['click'])\nconst hover = ref(false), press = ref(false), focus = ref(false)\nconst t = computed(() => ibSizes[props.size])\nconst busy = computed(() => props.disabled || props.loading)\nconst isPressed = computed(() => (press.value || props.pressed) && !busy.value)\nconst color = computed(() => props.danger ? 'var(--danger)' : props.muted ? 'var(--text-muted)' : 'var(--text-body)')\nconst bg = computed(() => isPressed.value ? (props.danger ? 'var(--danger-soft-active)' : 'var(--bg-press)') : props.active ? 'var(--bg-active)' : (hover.value && !busy.value ? (props.danger ? 'var(--danger-bg)' : 'var(--bg-hover)') : 'transparent'))\nconst s = computed(() => ({ width: t.value.box, height: t.value.box, borderRadius: t.value.radius, border: 'none', cursor: busy.value ? 'default' : 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', background: bg.value, color: color.value, opacity: busy.value ? .5 : 1, boxShadow: (focus.value || props.focused) && !busy.value ? 'var(--ring-focus)' : 'none', transition: 'var(--t-hover)', padding: 0, flexShrink: 0, ...props.style }))\n</script>\n<template>\n <button type=\"button\" data-press=\"own\" :aria-label=\"label\" :disabled=\"busy\" @click=\"$emit('click')\" @mouseenter=\"hover = true\" @mouseleave=\"hover = false; press = false\" @mousedown=\"press = true\" @mouseup=\"press = false\" @keydown.enter=\"press = true\" @keydown.space=\"press = true\" @keyup=\"press = false\" @focus=\"focus = $event.target.matches(':focus-visible')\" @blur=\"focus = false\" :style=\"s\">\n <span v-if=\"loading\" :style=\"{ display: 'inline-block', width: t.icon, height: t.icon, borderRadius: 'var(--radius-full)', border: '2px solid currentColor', borderTopColor: 'transparent', animation: 'kb-spin .7s linear infinite' }\" />\n <Icon v-else :name=\"icon\" :size=\"t.icon\" />\n </button>\n</template>\n","buttons/ButtonGroup.vue":"<script setup>\nimport { useSlots, computed } from 'vue'\nconst props = defineProps({ style: Object })\nconst slots = useSlots()\nconst items = computed(() => slots.default ? slots.default() : [])\n</script>\n<template>\n <div :style=\"{ display: 'inline-flex', ...style }\">\n <template v-for=\"(child, i) in items\" :key=\"i\">\n <component :is=\"child\" :style=\"{ borderRadius: 0,\n ...(i === 0 ? { borderTopLeftRadius: 'var(--radius-md)', borderBottomLeftRadius: 'var(--radius-md)' } : null),\n ...(i === items.length - 1 ? { borderTopRightRadius: 'var(--radius-md)', borderBottomRightRadius: 'var(--radius-md)' } : null),\n marginLeft: i === 0 ? 0 : -1 }\" />\n </template>\n </div>\n</template>\n"});
|