kanto-ds 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +8 -1
  2. package/dist/chunks/defaults-DKzFsNbv.js +57 -0
  3. package/dist/chunks/defaults-DKzFsNbv.js.map +1 -0
  4. package/dist/chunks/{listbox-CUXYn_3S.js → listbox-Sm3nvsC6.js} +5 -5
  5. package/dist/chunks/{listbox-CUXYn_3S.js.map → listbox-Sm3nvsC6.js.map} +1 -1
  6. package/dist/components/core/kt-icon/kt-icon.js +1 -1
  7. package/dist/components/core/kt-split-button/kt-split-button.d.ts +112 -0
  8. package/dist/components/core/kt-split-button/kt-split-button.js +262 -0
  9. package/dist/components/core/kt-split-button/kt-split-button.js.map +1 -0
  10. package/dist/components/data/kt-chart/cartesian.d.ts +62 -0
  11. package/dist/components/data/kt-chart/geometry.d.ts +41 -0
  12. package/dist/components/data/kt-chart/kt-chart.d.ts +79 -35
  13. package/dist/components/data/kt-chart/kt-chart.js +1200 -213
  14. package/dist/components/data/kt-chart/kt-chart.js.map +1 -1
  15. package/dist/components/data/kt-chart/model.d.ts +38 -0
  16. package/dist/components/data/kt-chart/point.d.ts +56 -0
  17. package/dist/components/data/kt-chart/radial.d.ts +60 -0
  18. package/dist/components/data/kt-chart/scale.d.ts +37 -0
  19. package/dist/components/forms/kt-input-menu/kt-input-menu.js +1 -1
  20. package/dist/components/forms/kt-select/kt-select.js +1 -1
  21. package/dist/components/overlays/kt-confirm-dialog/kt-confirm-dialog.js +41 -25
  22. package/dist/components/overlays/kt-confirm-dialog/kt-confirm-dialog.js.map +1 -1
  23. package/dist/components/overlays/kt-dropdown/kt-dropdown.d.ts +3 -0
  24. package/dist/components/overlays/kt-dropdown/kt-dropdown.js +48 -28
  25. package/dist/components/overlays/kt-dropdown/kt-dropdown.js.map +1 -1
  26. package/dist/components/overlays/kt-modal/kt-modal.js +12 -10
  27. package/dist/components/overlays/kt-modal/kt-modal.js.map +1 -1
  28. package/dist/icons/index.js +7 -34
  29. package/dist/icons/index.js.map +1 -1
  30. package/dist/index.d.ts +4 -2
  31. package/dist/index.js +89 -87
  32. package/dist/index.js.map +1 -1
  33. package/dist/react/index.d.ts +13 -0
  34. package/dist/react/index.js +176 -162
  35. package/dist/react/index.js.map +1 -1
  36. package/dist/vue/index.d.ts +2 -0
  37. package/dist/vue/index.js +8 -7
  38. package/dist/vue/index.js.map +1 -1
  39. package/package.json +8 -2
  40. package/dist/chunks/registry-X4LS9J5K.js +0 -30
  41. package/dist/chunks/registry-X4LS9J5K.js.map +0 -1
package/README.md CHANGED
@@ -25,6 +25,13 @@ import 'kanto-ds/styles.css';
25
25
 
26
26
  ---
27
27
 
28
+ ## Showcase
29
+
30
+ ![dashboard.png](demo/public/dashboard.png)
31
+
32
+ | ![guide.png](demo/public/guide.png) | ![component.png](demo/public/component.png) |
33
+ | ----------------------------------- | ------------------------------------------- |
34
+
28
35
  ## Why custom elements
29
36
 
30
37
  One implementation, every framework. The alternative — a React port, a Vue
@@ -79,7 +86,7 @@ Forty-two elements. Each has a page beside its source.
79
86
 
80
87
  | Group | Elements |
81
88
  | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
- | **Core** | [button](src/components/core/kt-button/kt-button.md) · [card](src/components/core/kt-card/kt-card.md) · [code](src/components/core/kt-code/kt-code.md) · [icon](src/components/core/kt-icon/kt-icon.md) · [avatar](src/components/core/kt-avatar/kt-avatar.md) · [badge](src/components/core/kt-badge/kt-badge.md) · [kbd](src/components/core/kt-kbd/kt-kbd.md) |
89
+ | **Core** | [button](src/components/core/kt-button/kt-button.md) · [split-button](src/components/core/kt-split-button/kt-split-button.md) · [card](src/components/core/kt-card/kt-card.md) · [code](src/components/core/kt-code/kt-code.md) · [icon](src/components/core/kt-icon/kt-icon.md) · [avatar](src/components/core/kt-avatar/kt-avatar.md) · [badge](src/components/core/kt-badge/kt-badge.md) · [kbd](src/components/core/kt-kbd/kt-kbd.md) |
83
90
  | **Forms** | [input](src/components/forms/kt-input/kt-input.md) · [textarea](src/components/forms/kt-textarea/kt-textarea.md) · [label-input](src/components/forms/kt-label-input/kt-label-input.md) · [form](src/components/forms/kt-form/kt-form.md) · [select](src/components/forms/kt-select/kt-select.md) · [input-menu](src/components/forms/kt-input-menu/kt-input-menu.md) · [toggle](src/components/forms/kt-toggle/kt-toggle.md) · [drag-drop](src/components/forms/kt-drag-drop/kt-drag-drop.md) |
84
91
  | **Navigation** | [header](src/components/navigation/kt-header/kt-header.md) · [breadcrumb](src/components/navigation/kt-breadcrumb/kt-breadcrumb.md) · [sub-menu-navigation](src/components/navigation/kt-sub-menu-navigation/kt-sub-menu-navigation.md) · [page-header](src/components/navigation/kt-page-header/kt-page-header.md) · [tabs](src/components/navigation/kt-tabs/kt-tabs.md) · [segmented-control](src/components/navigation/kt-segmented-control/kt-segmented-control.md) · [toggle-button](src/components/navigation/kt-toggle-button/kt-toggle-button.md) · [toggle-button-group](src/components/navigation/kt-toggle-button-group/kt-toggle-button-group.md) |
85
92
  | **Feedback** | [progress-bar](src/components/feedback/kt-progress-bar/kt-progress-bar.md) · [skeleton](src/components/feedback/kt-skeleton/kt-skeleton.md) · [tooltip](src/components/feedback/kt-tooltip/kt-tooltip.md) · [toast](src/components/feedback/kt-toast/kt-toast.md) · [toast-container + toaster](src/components/feedback/kt-toast-container/kt-toast-container.md) · [alert](src/components/feedback/kt-alert/kt-alert.md) · [empty-state](src/components/feedback/kt-empty-state/kt-empty-state.md) |
@@ -0,0 +1,57 @@
1
+ import { X as s, User as a, TriangleAlert as c, Trash2 as i, Search as f, Menu as g, LoaderCircle as p, Info as u, FileUp as y, File as A, EyeOff as C, Eye as l, Copy as I, CircleCheck as h, CircleAlert as w, ChevronUp as $, ChevronRight as b, ChevronLeft as d, ChevronDown as v, Check as U, ArrowUp as Z, ArrowDown as k } from "lucide";
2
+ const n = /* @__PURE__ */ new Map();
3
+ function t(e) {
4
+ return e.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-zA-Z])(\d)/g, "$1-$2").replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
5
+ }
6
+ function m(e, r) {
7
+ n.set(t(e), r);
8
+ }
9
+ function z(e) {
10
+ for (const [r, o] of Object.entries(e))
11
+ L(o) && m(r, o);
12
+ }
13
+ function E(e) {
14
+ return n.get(t(e));
15
+ }
16
+ function F() {
17
+ return [...n.keys()].sort();
18
+ }
19
+ function L(e) {
20
+ return Array.isArray(e) && e.every(
21
+ (r) => Array.isArray(r) && typeof r[0] == "string" && typeof r[1] == "object" && r[1] !== null
22
+ );
23
+ }
24
+ const j = {
25
+ ArrowDown: k,
26
+ ArrowUp: Z,
27
+ Check: U,
28
+ ChevronDown: v,
29
+ ChevronLeft: d,
30
+ ChevronRight: b,
31
+ ChevronUp: $,
32
+ CircleAlert: w,
33
+ CircleCheck: h,
34
+ Copy: I,
35
+ Eye: l,
36
+ EyeOff: C,
37
+ File: A,
38
+ FileUp: y,
39
+ Info: u,
40
+ LoaderCircle: p,
41
+ Menu: g,
42
+ Search: f,
43
+ Trash2: i,
44
+ TriangleAlert: c,
45
+ User: a,
46
+ X: s
47
+ };
48
+ z(j);
49
+ export {
50
+ z as a,
51
+ F as b,
52
+ j as d,
53
+ E as g,
54
+ m as r,
55
+ t
56
+ };
57
+ //# sourceMappingURL=defaults-DKzFsNbv.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defaults-DKzFsNbv.js","sources":["../../src/icons/registry.ts","../../src/icons/defaults.ts"],"sourcesContent":["/**\n * The icon registry.\n *\n * `<kt-icon>` looks icons up by name at render time, which means the set of\n * icons cannot be statically analysed and a bundler cannot tree-shake it. So\n * Kanto does not bundle all two thousand Lucide icons: the elements register\n * the handful they need themselves, and an application registers whatever else\n * it uses.\n *\n * import { Rocket, Wallet } from 'lucide';\n * import { registerIcons } from 'kanto-ds/icons';\n *\n * registerIcons({ Rocket, Wallet });\n *\n * Names are normalised to kebab-case, so `Rocket`, `rocket` and\n * `ChevronDown` / `chevron-down` all resolve.\n */\n\n/** A Lucide icon: a flat list of SVG children, each `[tag, attributes]`. */\nexport type IconNode = [tag: string, attrs: Record<string, string | number | undefined>][];\n\nconst registry = new Map<string, IconNode>();\n\n/**\n * `ChevronDown` → `chevron-down`, `Trash2` → `trash-2`, `AArrowDown` →\n * `a-arrow-down`.\n *\n * The digit boundary matters: Lucide names that icon `trash-2`, so a rule that\n * only splits on case produces `trash2` and the lookup quietly misses.\n */\nexport function toKebabCase(name: string): string {\n return name\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') // acronym run before a word\n .replace(/([a-zA-Z])(\\d)/g, '$1-$2') // letter before a digit\n .replace(/([a-z])([A-Z])/g, '$1-$2') // ordinary camel boundary\n .replace(/[\\s_]+/g, '-')\n .toLowerCase();\n}\n\n/** Registers a single icon under `name`. */\nexport function registerIcon(name: string, node: IconNode): void {\n registry.set(toKebabCase(name), node);\n}\n\n/**\n * Registers a map of icons. Accepts a Lucide namespace import directly:\n * every non-icon export (`createElement`, `createIcons`, …) is skipped.\n */\nexport function registerIcons(icons: Record<string, unknown>): void {\n for (const [name, node] of Object.entries(icons)) {\n if (isIconNode(node)) registerIcon(name, node);\n }\n}\n\n/** Returns the icon registered under `name`, or `undefined`. */\nexport function getIcon(name: string): IconNode | undefined {\n return registry.get(toKebabCase(name));\n}\n\n/** Every registered name, kebab-cased and sorted. Useful for docs and tests. */\nexport function registeredIcons(): string[] {\n return [...registry.keys()].sort();\n}\n\nfunction isIconNode(value: unknown): value is IconNode {\n return (\n Array.isArray(value) &&\n value.every(\n (child) =>\n Array.isArray(child) &&\n typeof child[0] === 'string' &&\n typeof child[1] === 'object' &&\n child[1] !== null,\n )\n );\n}\n","/**\n * The icons the design system uses in its own elements.\n *\n * Importing any Kanto element pulls this set in — roughly 2 kB of path data,\n * the price of `<kt-select>` being able to draw its own chevron. Application\n * icons are registered separately through `registerIcons`.\n */\nimport {\n ArrowDown,\n ArrowUp,\n Check,\n ChevronDown,\n ChevronLeft,\n ChevronRight,\n ChevronUp,\n CircleAlert,\n CircleCheck,\n Copy,\n Eye,\n EyeOff,\n File,\n FileUp,\n Info,\n LoaderCircle,\n Menu,\n Search,\n Trash2,\n TriangleAlert,\n User,\n X,\n} from 'lucide';\n// The sibling registry, not the `kanto-ds` barrel: the barrel pulls in every\n// element, and one of them pulls in this file — a cycle that leaves\n// `registerIcons` undefined by the time the call below runs.\nimport { registerIcons } from './registry.js';\n\n/** Icons referenced by Kanto elements, keyed by their Lucide name. */\nexport const defaultIcons = {\n ArrowDown,\n ArrowUp,\n Check,\n ChevronDown,\n ChevronLeft,\n ChevronRight,\n ChevronUp,\n CircleAlert,\n CircleCheck,\n Copy,\n Eye,\n EyeOff,\n File,\n FileUp,\n Info,\n LoaderCircle,\n Menu,\n Search,\n Trash2,\n TriangleAlert,\n User,\n X,\n};\n\nregisterIcons(defaultIcons);\n"],"names":["registry","toKebabCase","name","registerIcon","node","registerIcons","icons","isIconNode","getIcon","registeredIcons","value","child","defaultIcons","ArrowDown","ArrowUp","Check","ChevronDown","ChevronLeft","ChevronRight","ChevronUp","CircleAlert","CircleCheck","Copy","Eye","EyeOff","File","FileUp","Info","LoaderCircle","Menu","Search","Trash2","TriangleAlert","User","X"],"mappings":";AAqBA,MAAMA,wBAAe,IAAA;AASd,SAASC,EAAYC,GAAsB;AAChD,SAAOA,EACJ,QAAQ,yBAAyB,OAAO,EACxC,QAAQ,mBAAmB,OAAO,EAClC,QAAQ,mBAAmB,OAAO,EAClC,QAAQ,WAAW,GAAG,EACtB,YAAA;AACL;AAGO,SAASC,EAAaD,GAAcE,GAAsB;AAC/D,EAAAJ,EAAS,IAAIC,EAAYC,CAAI,GAAGE,CAAI;AACtC;AAMO,SAASC,EAAcC,GAAsC;AAClE,aAAW,CAACJ,GAAME,CAAI,KAAK,OAAO,QAAQE,CAAK;AAC7C,IAAIC,EAAWH,CAAI,KAAGD,EAAaD,GAAME,CAAI;AAEjD;AAGO,SAASI,EAAQN,GAAoC;AAC1D,SAAOF,EAAS,IAAIC,EAAYC,CAAI,CAAC;AACvC;AAGO,SAASO,IAA4B;AAC1C,SAAO,CAAC,GAAGT,EAAS,KAAA,CAAM,EAAE,KAAA;AAC9B;AAEA,SAASO,EAAWG,GAAmC;AACrD,SACE,MAAM,QAAQA,CAAK,KACnBA,EAAM;AAAA,IACJ,CAACC,MACC,MAAM,QAAQA,CAAK,KACnB,OAAOA,EAAM,CAAC,KAAM,YACpB,OAAOA,EAAM,CAAC,KAAM,YACpBA,EAAM,CAAC,MAAM;AAAA,EAAA;AAGrB;ACtCO,MAAMC,IAAe;AAAA,EAC1B,WAAAC;AAAA,EACA,SAAAC;AAAA,EACA,OAAAC;AAAA,EACA,aAAAC;AAAA,EACA,aAAAC;AAAA,EACA,cAAAC;AAAA,EACA,WAAAC;AAAA,EACA,aAAAC;AAAA,EACA,aAAAC;AAAA,EACA,MAAAC;AAAA,EACA,KAAAC;AAAA,EACA,QAAAC;AAAA,EACA,MAAAC;AAAA,EACA,QAAAC;AAAA,EACA,MAAAC;AAAA,EACA,cAAAC;AAAA,EACA,MAAAC;AAAA,EACA,QAAAC;AAAA,EACA,QAAAC;AAAA,EACA,eAAAC;AAAA,EACA,MAAAC;AAAA,EACA,GAAAC;AACF;AAEA7B,EAAcO,CAAY;"}
@@ -91,11 +91,11 @@ const f = i`
91
91
  }
92
92
  `;
93
93
  export {
94
- p as a,
95
- u as b,
96
- l as f,
97
- f as l,
94
+ f as a,
95
+ l as b,
96
+ p as f,
97
+ u as l,
98
98
  s as n,
99
99
  o
100
100
  };
101
- //# sourceMappingURL=listbox-CUXYn_3S.js.map
101
+ //# sourceMappingURL=listbox-Sm3nvsC6.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"listbox-CUXYn_3S.js","sources":["../../src/internal/listbox.ts"],"sourcesContent":["import { css } from 'lit';\n\n/**\n * Shared pieces of the two option-list controls, `<kt-select>` and\n * `<kt-input-menu>`.\n *\n * They differ in how an option is chosen — pick from a list, or filter by\n * typing — but the popup, the option rows and the keyboard model are the same,\n * and having them drift apart is how a design system stops looking like one.\n */\n\nexport interface KtOption {\n readonly id: string | number;\n readonly label?: string;\n readonly disabled?: boolean;\n}\n\n/** The text to display for an option: its label, or its id as a fallback. */\nexport function optionLabel(option: KtOption): string {\n return option.label ?? String(option.id);\n}\n\n/** Case-insensitive substring match on the visible label. */\nexport function filterOptions(options: readonly KtOption[], query: string): readonly KtOption[] {\n const needle = query.trim().toLowerCase();\n if (!needle) return options;\n return options.filter((option) => optionLabel(option).toLowerCase().includes(needle));\n}\n\n/**\n * The next selectable index in `direction`, skipping disabled options and\n * wrapping at both ends. Returns -1 when every option is disabled, so a caller\n * never lands the active descendant on something that cannot be chosen.\n */\nexport function nextEnabledIndex(\n options: readonly KtOption[],\n from: number,\n direction: 1 | -1,\n): number {\n if (options.length === 0) return -1;\n\n for (let step = 1; step <= options.length; step += 1) {\n const index = (from + direction * step + options.length * step) % options.length;\n if (!options[index]?.disabled) return index;\n }\n return -1;\n}\n\n/** The first selectable index, for opening a list with nothing selected. */\nexport function firstEnabledIndex(options: readonly KtOption[]): number {\n return options.findIndex((option) => !option.disabled);\n}\n\n/** The last selectable index, for End. */\nexport function lastEnabledIndex(options: readonly KtOption[]): number {\n for (let index = options.length - 1; index >= 0; index -= 1) {\n if (!options[index]?.disabled) return index;\n }\n return -1;\n}\n\n/**\n * The popup and option-row styles.\n *\n * The panel animates through opacity and transform while staying in the layout\n * as `visibility: hidden`, rather than being added and removed: a popup that\n * unmounts cannot animate out, and one that only sets `display: none` cannot\n * animate at all.\n */\nexport const listboxStyles = css`\n .popup {\n position: absolute;\n top: calc(var(--field-height, var(--button-height)) + 6px);\n left: 0;\n z-index: var(--z-dropdown);\n display: flex;\n flex-direction: column;\n gap: var(--gap-element);\n box-sizing: border-box;\n width: 100%;\n max-height: 300px;\n margin: 0;\n padding: var(--padding-expand);\n overflow: auto;\n list-style: none;\n background: var(--color-dark-20);\n border-radius: var(--radius-input);\n scrollbar-width: thin;\n\n visibility: hidden;\n opacity: 0;\n transform: translateY(-6px) scaleY(0.98);\n transform-origin: top left;\n pointer-events: none;\n transition:\n opacity var(--duration-fast) var(--easing-standard),\n transform var(--duration-fast) var(--easing-standard);\n }\n\n .popup.open {\n visibility: visible;\n opacity: 1;\n transform: translateY(0) scaleY(1);\n pointer-events: auto;\n }\n\n .option {\n padding: var(--padding-expand-item);\n color: var(--text-muted);\n font: var(--font-input);\n border-radius: var(--radius-input);\n cursor: pointer;\n }\n\n .option:hover,\n .option.active {\n background: var(--color-dark-22);\n }\n\n .option.selected {\n color: var(--text-body);\n font-weight: 600;\n background: var(--color-dark-23);\n }\n\n .option.disabled {\n opacity: 0.5;\n pointer-events: none;\n }\n\n .empty {\n padding: var(--padding-expand-item);\n color: var(--text-muted);\n font: var(--font-normal-small);\n pointer-events: none;\n }\n`;\n"],"names":["optionLabel","option","filterOptions","options","query","needle","nextEnabledIndex","from","direction","step","index","firstEnabledIndex","lastEnabledIndex","listboxStyles","css"],"mappings":";AAkBO,SAASA,EAAYC,GAA0B;AACpD,SAAOA,EAAO,SAAS,OAAOA,EAAO,EAAE;AACzC;AAGO,SAASC,EAAcC,GAA8BC,GAAoC;AAC9F,QAAMC,IAASD,EAAM,KAAA,EAAO,YAAA;AAC5B,SAAKC,IACEF,EAAQ,OAAO,CAACF,MAAWD,EAAYC,CAAM,EAAE,YAAA,EAAc,SAASI,CAAM,CAAC,IADhEF;AAEtB;AAOO,SAASG,EACdH,GACAI,GACAC,GACQ;AACR,MAAIL,EAAQ,WAAW,EAAG,QAAO;AAEjC,WAASM,IAAO,GAAGA,KAAQN,EAAQ,QAAQM,KAAQ,GAAG;AACpD,UAAMC,KAASH,IAAOC,IAAYC,IAAON,EAAQ,SAASM,KAAQN,EAAQ;AAC1E,QAAI,CAACA,EAAQO,CAAK,GAAG,SAAU,QAAOA;AAAA,EACxC;AACA,SAAO;AACT;AAGO,SAASC,EAAkBR,GAAsC;AACtE,SAAOA,EAAQ,UAAU,CAACF,MAAW,CAACA,EAAO,QAAQ;AACvD;AAGO,SAASW,EAAiBT,GAAsC;AACrE,WAASO,IAAQP,EAAQ,SAAS,GAAGO,KAAS,GAAGA,KAAS;AACxD,QAAI,CAACP,EAAQO,CAAK,GAAG,SAAU,QAAOA;AAExC,SAAO;AACT;AAUO,MAAMG,IAAgBC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;"}
1
+ {"version":3,"file":"listbox-Sm3nvsC6.js","sources":["../../src/internal/listbox.ts"],"sourcesContent":["import { css } from 'lit';\n\n/**\n * Shared pieces of the two option-list controls, `<kt-select>` and\n * `<kt-input-menu>`.\n *\n * They differ in how an option is chosen — pick from a list, or filter by\n * typing — but the popup, the option rows and the keyboard model are the same,\n * and having them drift apart is how a design system stops looking like one.\n */\n\nexport interface KtOption {\n readonly id: string | number;\n readonly label?: string;\n readonly disabled?: boolean;\n}\n\n/** The text to display for an option: its label, or its id as a fallback. */\nexport function optionLabel(option: KtOption): string {\n return option.label ?? String(option.id);\n}\n\n/** Case-insensitive substring match on the visible label. */\nexport function filterOptions(options: readonly KtOption[], query: string): readonly KtOption[] {\n const needle = query.trim().toLowerCase();\n if (!needle) return options;\n return options.filter((option) => optionLabel(option).toLowerCase().includes(needle));\n}\n\n/**\n * The next selectable index in `direction`, skipping disabled options and\n * wrapping at both ends. Returns -1 when every option is disabled, so a caller\n * never lands the active descendant on something that cannot be chosen.\n */\nexport function nextEnabledIndex(\n options: readonly KtOption[],\n from: number,\n direction: 1 | -1,\n): number {\n if (options.length === 0) return -1;\n\n for (let step = 1; step <= options.length; step += 1) {\n const index = (from + direction * step + options.length * step) % options.length;\n if (!options[index]?.disabled) return index;\n }\n return -1;\n}\n\n/** The first selectable index, for opening a list with nothing selected. */\nexport function firstEnabledIndex(options: readonly KtOption[]): number {\n return options.findIndex((option) => !option.disabled);\n}\n\n/** The last selectable index, for End. */\nexport function lastEnabledIndex(options: readonly KtOption[]): number {\n for (let index = options.length - 1; index >= 0; index -= 1) {\n if (!options[index]?.disabled) return index;\n }\n return -1;\n}\n\n/**\n * The popup and option-row styles.\n *\n * The panel animates through opacity and transform while staying in the layout\n * as `visibility: hidden`, rather than being added and removed: a popup that\n * unmounts cannot animate out, and one that only sets `display: none` cannot\n * animate at all.\n */\nexport const listboxStyles = css`\n .popup {\n position: absolute;\n top: calc(var(--field-height, var(--button-height)) + 6px);\n left: 0;\n z-index: var(--z-dropdown);\n display: flex;\n flex-direction: column;\n gap: var(--gap-element);\n box-sizing: border-box;\n width: 100%;\n max-height: 300px;\n margin: 0;\n padding: var(--padding-expand);\n overflow: auto;\n list-style: none;\n background: var(--color-dark-20);\n border-radius: var(--radius-input);\n scrollbar-width: thin;\n\n visibility: hidden;\n opacity: 0;\n transform: translateY(-6px) scaleY(0.98);\n transform-origin: top left;\n pointer-events: none;\n transition:\n opacity var(--duration-fast) var(--easing-standard),\n transform var(--duration-fast) var(--easing-standard);\n }\n\n .popup.open {\n visibility: visible;\n opacity: 1;\n transform: translateY(0) scaleY(1);\n pointer-events: auto;\n }\n\n .option {\n padding: var(--padding-expand-item);\n color: var(--text-muted);\n font: var(--font-input);\n border-radius: var(--radius-input);\n cursor: pointer;\n }\n\n .option:hover,\n .option.active {\n background: var(--color-dark-22);\n }\n\n .option.selected {\n color: var(--text-body);\n font-weight: 600;\n background: var(--color-dark-23);\n }\n\n .option.disabled {\n opacity: 0.5;\n pointer-events: none;\n }\n\n .empty {\n padding: var(--padding-expand-item);\n color: var(--text-muted);\n font: var(--font-normal-small);\n pointer-events: none;\n }\n`;\n"],"names":["optionLabel","option","filterOptions","options","query","needle","nextEnabledIndex","from","direction","step","index","firstEnabledIndex","lastEnabledIndex","listboxStyles","css"],"mappings":";AAkBO,SAASA,EAAYC,GAA0B;AACpD,SAAOA,EAAO,SAAS,OAAOA,EAAO,EAAE;AACzC;AAGO,SAASC,EAAcC,GAA8BC,GAAoC;AAC9F,QAAMC,IAASD,EAAM,KAAA,EAAO,YAAA;AAC5B,SAAKC,IACEF,EAAQ,OAAO,CAACF,MAAWD,EAAYC,CAAM,EAAE,YAAA,EAAc,SAASI,CAAM,CAAC,IADhEF;AAEtB;AAOO,SAASG,EACdH,GACAI,GACAC,GACQ;AACR,MAAIL,EAAQ,WAAW,EAAG,QAAO;AAEjC,WAASM,IAAO,GAAGA,KAAQN,EAAQ,QAAQM,KAAQ,GAAG;AACpD,UAAMC,KAASH,IAAOC,IAAYC,IAAON,EAAQ,SAASM,KAAQN,EAAQ;AAC1E,QAAI,CAACA,EAAQO,CAAK,GAAG,SAAU,QAAOA;AAAA,EACxC;AACA,SAAO;AACT;AAGO,SAASC,EAAkBR,GAAsC;AACtE,SAAOA,EAAQ,UAAU,CAACF,MAAW,CAACA,EAAO,QAAQ;AACvD;AAGO,SAASW,EAAiBT,GAAsC;AACrE,WAASO,IAAQP,EAAQ,SAAS,GAAGO,KAAS,GAAGA,KAAS;AACxD,QAAI,CAACP,EAAQO,CAAK,GAAG,SAAU,QAAOA;AAExC,SAAO;AACT;AAUO,MAAMG,IAAgBC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;"}
@@ -1,7 +1,7 @@
1
1
  import { css as f, html as c, nothing as l } from "lit";
2
2
  import { property as n } from "lit/decorators.js";
3
3
  import { K as m, d as u } from "../../../chunks/kt-element-CcMLO5e8.js";
4
- import { g as v } from "../../../chunks/registry-X4LS9J5K.js";
4
+ import { g as v } from "../../../chunks/defaults-DKzFsNbv.js";
5
5
  var w = Object.defineProperty, o = (i, e, r, h) => {
6
6
  for (var t = void 0, a = i.length - 1, d; a >= 0; a--)
7
7
  (d = i[a]) && (t = d(e, r, t) || t);
@@ -0,0 +1,112 @@
1
+ import { type TemplateResult } from 'lit';
2
+ import { KtElement } from '#internal/kt-element';
3
+ import { type KtOption } from '#internal/listbox';
4
+ import type { KtButtonSize, KtButtonVariant } from '../kt-button/kt-button.js';
5
+ import type { KtDropdownPlacement } from '../../overlays/kt-dropdown/kt-dropdown.js';
6
+ import '../kt-button/kt-button.js';
7
+ import '../kt-icon/kt-icon.js';
8
+ import '../../overlays/kt-dropdown/kt-dropdown.js';
9
+ /** A row of the menu. `danger` tints it, for the one action to think twice about. */
10
+ export interface KtSplitButtonItem extends KtOption {
11
+ readonly icon?: string;
12
+ readonly variant?: 'default' | 'danger';
13
+ }
14
+ /**
15
+ * One button carrying a menu of related actions.
16
+ *
17
+ * The body runs the action a user wants nine times out of ten and emits a
18
+ * plain `click`; the caret opens the rest. Reach for it when the alternatives
19
+ * are variations on the same action — "Save and close", "Save a copy" — and
20
+ * for an unrelated set of actions use `<kt-dropdown>` instead, which has no
21
+ * default action to promote.
22
+ *
23
+ * A caret press never reads as a `click` on the host, so a single `click`
24
+ * listener means the primary action and nothing else.
25
+ *
26
+ * @element kt-split-button
27
+ *
28
+ * @slot - The primary action's label.
29
+ *
30
+ * @csspart action - The primary `<kt-button>`.
31
+ * @csspart caret - The `<kt-button>` that opens the menu.
32
+ * @csspart menu - The list of rows.
33
+ * @csspart item - A menu row.
34
+ *
35
+ * @fires kt-select - A menu row was chosen. `detail: { value, item }`.
36
+ * @fires kt-open - The menu opened.
37
+ * @fires kt-close - The menu closed.
38
+ *
39
+ * @example
40
+ * ```html
41
+ * <kt-split-button>Save</kt-split-button>
42
+ * ```
43
+ * ```js
44
+ * el.items = [
45
+ * { id: 'close', label: 'Save and close' },
46
+ * { id: 'discard', label: 'Discard', variant: 'danger' },
47
+ * ];
48
+ * el.addEventListener('kt-select', (e) => run(e.detail.value));
49
+ * ```
50
+ */
51
+ export declare class KtSplitButton extends KtElement {
52
+ static styles: import("lit").CSSResultGroup[];
53
+ private dropdown?;
54
+ private caret?;
55
+ /** The row the roving tabindex currently sits on. */
56
+ private activeIndex;
57
+ /** The menu rows. */
58
+ items: readonly KtSplitButtonItem[];
59
+ /** Visual variant, forwarded to both segments. */
60
+ variant: KtButtonVariant;
61
+ /** Height, forwarded to both segments. */
62
+ size: KtButtonSize;
63
+ disabled: boolean;
64
+ /** Lucide icon name for the primary segment. */
65
+ icon: string;
66
+ /** Accessible name of the caret, which has no text of its own. */
67
+ menuLabel: string;
68
+ /** Where to put the menu when there is room on both sides. */
69
+ preferredPlacement: KtDropdownPlacement;
70
+ emptyText: string;
71
+ /** Whether the menu is showing. */
72
+ get isOpen(): boolean;
73
+ /** Opens the menu. */
74
+ show(): void;
75
+ /** Closes it. */
76
+ hide(): void;
77
+ /** Either. */
78
+ toggle(): void;
79
+ /** Moves focus to the primary segment. */
80
+ focus(options?: FocusOptions): void;
81
+ private row;
82
+ /** Lands the roving tabindex on `index` and takes focus there. */
83
+ private moveTo;
84
+ private onOpen;
85
+ /**
86
+ * Returns focus to the caret, but only when it was inside the menu — an
87
+ * outside click already moved it somewhere the user chose, and pulling it
88
+ * back here would undo that.
89
+ */
90
+ private onClose;
91
+ private onMenuKeyDown;
92
+ private choose;
93
+ /**
94
+ * A click anywhere in the menu stops here. It originates inside this
95
+ * element's shadow root, so without this it would surface on the host as a
96
+ * `click` and be mistaken for the primary action.
97
+ */
98
+ private containClick;
99
+ /**
100
+ * The caret drives the panel itself rather than leaning on the click
101
+ * reaching `<kt-dropdown>`'s own trigger through the slot: stopping here
102
+ * keeps the press off the host, and leaving it to bubble would toggle twice.
103
+ */
104
+ private onCaretClick;
105
+ private renderItem;
106
+ render(): TemplateResult;
107
+ }
108
+ declare global {
109
+ interface HTMLElementTagNameMap {
110
+ 'kt-split-button': KtSplitButton;
111
+ }
112
+ }
@@ -0,0 +1,262 @@
1
+ import { css as f, nothing as c, html as a } from "lit";
2
+ import { query as b, state as g, property as o } from "lit/decorators.js";
3
+ import { classMap as y } from "lit/directives/class-map.js";
4
+ import { K as h, d as w } from "../../../chunks/kt-element-CcMLO5e8.js";
5
+ import { e as x } from "../../../chunks/events-Bjl5Zmmq.js";
6
+ import { f as m, o as k, l as $, n as u } from "../../../chunks/listbox-Sm3nvsC6.js";
7
+ import "../kt-button/kt-button.js";
8
+ import "../kt-icon/kt-icon.js";
9
+ import "../../overlays/kt-dropdown/kt-dropdown.js";
10
+ var I = Object.defineProperty, i = (n, t, r, l) => {
11
+ for (var s = void 0, d = n.length - 1, p; d >= 0; d--)
12
+ (p = n[d]) && (s = p(t, r, s) || s);
13
+ return s && I(t, r, s), s;
14
+ };
15
+ const v = { small: 16, medium: 20, large: 24 };
16
+ class e extends h {
17
+ constructor() {
18
+ super(...arguments), this.activeIndex = -1, this.items = [], this.variant = "primary", this.size = "medium", this.disabled = !1, this.icon = "", this.menuLabel = "More actions", this.preferredPlacement = "bottom", this.emptyText = "No actions", this.onOpen = () => {
19
+ this.moveTo(m(this.items));
20
+ }, this.onClose = () => {
21
+ const t = this.shadowRoot?.activeElement;
22
+ this.activeIndex = -1, t instanceof HTMLElement && t.classList.contains("item") && this.caret?.focus();
23
+ }, this.onMenuKeyDown = (t) => {
24
+ const l = {
25
+ ArrowDown: () => u(this.items, this.activeIndex, 1),
26
+ ArrowUp: () => u(this.items, this.activeIndex, -1),
27
+ Home: () => m(this.items),
28
+ End: () => $(this.items)
29
+ }[t.key];
30
+ l && (t.preventDefault(), this.moveTo(l()));
31
+ };
32
+ }
33
+ static {
34
+ this.styles = [
35
+ h.styles,
36
+ f`
37
+ :host {
38
+ display: inline-flex;
39
+ vertical-align: middle;
40
+ }
41
+
42
+ /* A hairline of the page showing through is the seam between the two
43
+ segments — the same trick the rest of the system uses for elevation,
44
+ where a border would have been. */
45
+ .split {
46
+ display: inline-flex;
47
+ align-items: stretch;
48
+ gap: var(--border-width);
49
+ }
50
+
51
+ .action::part(button) {
52
+ border-top-right-radius: 0;
53
+ border-bottom-right-radius: 0;
54
+ }
55
+
56
+ .caret::part(button) {
57
+ border-top-left-radius: 0;
58
+ border-bottom-left-radius: 0;
59
+ }
60
+
61
+ .menu {
62
+ display: flex;
63
+ flex-direction: column;
64
+ gap: var(--gap-element);
65
+ max-height: 208px;
66
+ overflow: auto;
67
+ scrollbar-width: thin;
68
+ }
69
+
70
+ .item {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: var(--gap-button);
74
+ width: 100%;
75
+ padding: var(--padding-expand-item);
76
+ color: var(--text-muted);
77
+ font: var(--font-normal-regular);
78
+ text-align: left;
79
+ background: none;
80
+ border: none;
81
+ border-radius: var(--radius-input);
82
+ cursor: pointer;
83
+ }
84
+
85
+ .item:hover {
86
+ color: var(--text-body);
87
+ background: var(--surface-hover);
88
+ }
89
+
90
+ /* Inset, so the ring is never clipped by the panel it scrolls inside. */
91
+ .item:focus-visible {
92
+ outline: var(--outline-width) solid var(--color-primary-base);
93
+ outline-offset: calc(-1 * var(--outline-width));
94
+ }
95
+
96
+ .item.danger {
97
+ color: var(--color-danger-base);
98
+ }
99
+ .item.danger:hover {
100
+ color: var(--color-danger-base);
101
+ background: var(--color-danger-soft);
102
+ }
103
+
104
+ .item[aria-disabled='true'] {
105
+ color: var(--text-disabled);
106
+ cursor: not-allowed;
107
+ }
108
+ .item[aria-disabled='true']:hover {
109
+ color: var(--text-disabled);
110
+ background: none;
111
+ }
112
+
113
+ .empty {
114
+ padding: var(--padding-expand-item);
115
+ color: var(--text-muted);
116
+ font: var(--font-normal-small);
117
+ cursor: default;
118
+ }
119
+ `
120
+ ];
121
+ }
122
+ /** Whether the menu is showing. */
123
+ get isOpen() {
124
+ return this.dropdown?.isOpen ?? !1;
125
+ }
126
+ /** Opens the menu. */
127
+ show() {
128
+ this.dropdown?.show();
129
+ }
130
+ /** Closes it. */
131
+ hide() {
132
+ this.dropdown?.hide();
133
+ }
134
+ /** Either. */
135
+ toggle() {
136
+ this.dropdown?.toggle();
137
+ }
138
+ /** Moves focus to the primary segment. */
139
+ focus(t) {
140
+ this.shadowRoot?.querySelector(".action")?.focus(t);
141
+ }
142
+ row(t) {
143
+ return this.shadowRoot?.querySelectorAll(".item")[t] ?? null;
144
+ }
145
+ /** Lands the roving tabindex on `index` and takes focus there. */
146
+ async moveTo(t) {
147
+ t < 0 || (this.activeIndex = t, await this.updateComplete, this.row(t)?.focus());
148
+ }
149
+ choose(t) {
150
+ t.disabled || (this.hide(), x(this, "kt-select", { value: t.id, item: t }));
151
+ }
152
+ /**
153
+ * A click anywhere in the menu stops here. It originates inside this
154
+ * element's shadow root, so without this it would surface on the host as a
155
+ * `click` and be mistaken for the primary action.
156
+ */
157
+ containClick(t) {
158
+ t.stopPropagation();
159
+ }
160
+ /**
161
+ * The caret drives the panel itself rather than leaning on the click
162
+ * reaching `<kt-dropdown>`'s own trigger through the slot: stopping here
163
+ * keeps the press off the host, and leaving it to bubble would toggle twice.
164
+ */
165
+ onCaretClick(t) {
166
+ t.stopPropagation(), this.toggle();
167
+ }
168
+ renderItem(t, r) {
169
+ return a`<button
170
+ part="item"
171
+ class=${y({ item: !0, danger: t.variant === "danger" })}
172
+ type="button"
173
+ role="menuitem"
174
+ tabindex=${r === this.activeIndex ? 0 : -1}
175
+ aria-disabled=${t.disabled ? "true" : c}
176
+ @click=${() => this.choose(t)}
177
+ >
178
+ ${t.icon ? a`<kt-icon name=${t.icon} size=${v[this.size]}></kt-icon>` : c}
179
+ ${k(t)}
180
+ </button>`;
181
+ }
182
+ render() {
183
+ return a`<div class="split">
184
+ <!-- The icon is slotted rather than passed as \`icon\`: <kt-button> reads
185
+ its own text content to decide whether it is icon-only, and a <slot>
186
+ has none, so setting \`icon\` here would collapse it to a square. -->
187
+ <kt-button
188
+ part="action"
189
+ class="action"
190
+ variant=${this.variant}
191
+ size=${this.size}
192
+ ?disabled=${this.disabled}
193
+ >
194
+ ${this.icon ? a`<kt-icon name=${this.icon} size=${v[this.size]}></kt-icon>` : c}
195
+ <slot></slot>
196
+ </kt-button>
197
+
198
+ <kt-dropdown
199
+ align="end"
200
+ preferred-placement=${this.preferredPlacement}
201
+ ?disabled=${this.disabled}
202
+ @click=${this.containClick}
203
+ @kt-open=${this.onOpen}
204
+ @kt-close=${this.onClose}
205
+ >
206
+ <kt-button
207
+ slot="trigger"
208
+ part="caret"
209
+ class="caret"
210
+ variant=${this.variant}
211
+ size=${this.size}
212
+ icon="chevron-down"
213
+ label=${this.menuLabel}
214
+ ?disabled=${this.disabled}
215
+ @click=${this.onCaretClick}
216
+ ></kt-button>
217
+
218
+ <div slot="panel" part="menu" class="menu" role="menu" @keydown=${this.onMenuKeyDown}>
219
+ ${this.items.length > 0 ? this.items.map((t, r) => this.renderItem(t, r)) : a`<div class="empty">${this.emptyText}</div>`}
220
+ </div>
221
+ </kt-dropdown>
222
+ </div>`;
223
+ }
224
+ }
225
+ i([
226
+ b("kt-dropdown")
227
+ ], e.prototype, "dropdown");
228
+ i([
229
+ b(".caret")
230
+ ], e.prototype, "caret");
231
+ i([
232
+ g()
233
+ ], e.prototype, "activeIndex");
234
+ i([
235
+ o({ attribute: !1 })
236
+ ], e.prototype, "items");
237
+ i([
238
+ o({ type: String, reflect: !0 })
239
+ ], e.prototype, "variant");
240
+ i([
241
+ o({ type: String, reflect: !0 })
242
+ ], e.prototype, "size");
243
+ i([
244
+ o({ type: Boolean, reflect: !0 })
245
+ ], e.prototype, "disabled");
246
+ i([
247
+ o({ type: String })
248
+ ], e.prototype, "icon");
249
+ i([
250
+ o({ type: String, attribute: "menu-label" })
251
+ ], e.prototype, "menuLabel");
252
+ i([
253
+ o({ type: String, attribute: "preferred-placement" })
254
+ ], e.prototype, "preferredPlacement");
255
+ i([
256
+ o({ type: String, attribute: "empty-text" })
257
+ ], e.prototype, "emptyText");
258
+ w("kt-split-button", e);
259
+ export {
260
+ e as KtSplitButton
261
+ };
262
+ //# sourceMappingURL=kt-split-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kt-split-button.js","sources":["../../../../src/components/core/kt-split-button/kt-split-button.ts"],"sourcesContent":["import { css, html, nothing, type TemplateResult } from 'lit';\nimport { property, query, state } from 'lit/decorators.js';\nimport { classMap } from 'lit/directives/class-map.js';\nimport { KtElement, defineElement } from '#internal/kt-element';\nimport { emit } from '#internal/events';\nimport {\n firstEnabledIndex,\n lastEnabledIndex,\n nextEnabledIndex,\n optionLabel,\n type KtOption,\n} from '#internal/listbox';\nimport type { KtButtonSize, KtButtonVariant } from '../kt-button/kt-button.js';\nimport type { KtDropdown, KtDropdownPlacement } from '../../overlays/kt-dropdown/kt-dropdown.js';\nimport '../kt-button/kt-button.js';\nimport '../kt-icon/kt-icon.js';\nimport '../../overlays/kt-dropdown/kt-dropdown.js';\n\n/** A row of the menu. `danger` tints it, for the one action to think twice about. */\nexport interface KtSplitButtonItem extends KtOption {\n readonly icon?: string;\n readonly variant?: 'default' | 'danger';\n}\n\n/** Icon size per button size, matching `<kt-button>`. */\nconst ICON_SIZE: Record<KtButtonSize, number> = { small: 16, medium: 20, large: 24 };\n\n/**\n * One button carrying a menu of related actions.\n *\n * The body runs the action a user wants nine times out of ten and emits a\n * plain `click`; the caret opens the rest. Reach for it when the alternatives\n * are variations on the same action — \"Save and close\", \"Save a copy\" — and\n * for an unrelated set of actions use `<kt-dropdown>` instead, which has no\n * default action to promote.\n *\n * A caret press never reads as a `click` on the host, so a single `click`\n * listener means the primary action and nothing else.\n *\n * @element kt-split-button\n *\n * @slot - The primary action's label.\n *\n * @csspart action - The primary `<kt-button>`.\n * @csspart caret - The `<kt-button>` that opens the menu.\n * @csspart menu - The list of rows.\n * @csspart item - A menu row.\n *\n * @fires kt-select - A menu row was chosen. `detail: { value, item }`.\n * @fires kt-open - The menu opened.\n * @fires kt-close - The menu closed.\n *\n * @example\n * ```html\n * <kt-split-button>Save</kt-split-button>\n * ```\n * ```js\n * el.items = [\n * { id: 'close', label: 'Save and close' },\n * { id: 'discard', label: 'Discard', variant: 'danger' },\n * ];\n * el.addEventListener('kt-select', (e) => run(e.detail.value));\n * ```\n */\nexport class KtSplitButton extends KtElement {\n static override styles = [\n KtElement.styles,\n css`\n :host {\n display: inline-flex;\n vertical-align: middle;\n }\n\n /* A hairline of the page showing through is the seam between the two\n segments — the same trick the rest of the system uses for elevation,\n where a border would have been. */\n .split {\n display: inline-flex;\n align-items: stretch;\n gap: var(--border-width);\n }\n\n .action::part(button) {\n border-top-right-radius: 0;\n border-bottom-right-radius: 0;\n }\n\n .caret::part(button) {\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n }\n\n .menu {\n display: flex;\n flex-direction: column;\n gap: var(--gap-element);\n max-height: 208px;\n overflow: auto;\n scrollbar-width: thin;\n }\n\n .item {\n display: flex;\n align-items: center;\n gap: var(--gap-button);\n width: 100%;\n padding: var(--padding-expand-item);\n color: var(--text-muted);\n font: var(--font-normal-regular);\n text-align: left;\n background: none;\n border: none;\n border-radius: var(--radius-input);\n cursor: pointer;\n }\n\n .item:hover {\n color: var(--text-body);\n background: var(--surface-hover);\n }\n\n /* Inset, so the ring is never clipped by the panel it scrolls inside. */\n .item:focus-visible {\n outline: var(--outline-width) solid var(--color-primary-base);\n outline-offset: calc(-1 * var(--outline-width));\n }\n\n .item.danger {\n color: var(--color-danger-base);\n }\n .item.danger:hover {\n color: var(--color-danger-base);\n background: var(--color-danger-soft);\n }\n\n .item[aria-disabled='true'] {\n color: var(--text-disabled);\n cursor: not-allowed;\n }\n .item[aria-disabled='true']:hover {\n color: var(--text-disabled);\n background: none;\n }\n\n .empty {\n padding: var(--padding-expand-item);\n color: var(--text-muted);\n font: var(--font-normal-small);\n cursor: default;\n }\n `,\n ];\n\n @query('kt-dropdown')\n private dropdown?: KtDropdown;\n\n @query('.caret')\n private caret?: HTMLElement & { focus(): void };\n\n /** The row the roving tabindex currently sits on. */\n @state()\n private activeIndex = -1;\n\n /** The menu rows. */\n @property({ attribute: false })\n items: readonly KtSplitButtonItem[] = [];\n\n /** Visual variant, forwarded to both segments. */\n @property({ type: String, reflect: true })\n variant: KtButtonVariant = 'primary';\n\n /** Height, forwarded to both segments. */\n @property({ type: String, reflect: true })\n size: KtButtonSize = 'medium';\n\n @property({ type: Boolean, reflect: true })\n disabled = false;\n\n /** Lucide icon name for the primary segment. */\n @property({ type: String })\n icon = '';\n\n /** Accessible name of the caret, which has no text of its own. */\n @property({ type: String, attribute: 'menu-label' })\n menuLabel = 'More actions';\n\n /** Where to put the menu when there is room on both sides. */\n @property({ type: String, attribute: 'preferred-placement' })\n preferredPlacement: KtDropdownPlacement = 'bottom';\n\n @property({ type: String, attribute: 'empty-text' })\n emptyText = 'No actions';\n\n /** Whether the menu is showing. */\n get isOpen(): boolean {\n return this.dropdown?.isOpen ?? false;\n }\n\n /** Opens the menu. */\n show(): void {\n this.dropdown?.show();\n }\n\n /** Closes it. */\n hide(): void {\n this.dropdown?.hide();\n }\n\n /** Either. */\n toggle(): void {\n this.dropdown?.toggle();\n }\n\n /** Moves focus to the primary segment. */\n override focus(options?: FocusOptions): void {\n this.shadowRoot?.querySelector<HTMLElement>('.action')?.focus(options);\n }\n\n private row(index: number): HTMLElement | null {\n return this.shadowRoot?.querySelectorAll<HTMLElement>('.item')[index] ?? null;\n }\n\n /** Lands the roving tabindex on `index` and takes focus there. */\n private async moveTo(index: number): Promise<void> {\n if (index < 0) return;\n this.activeIndex = index;\n await this.updateComplete;\n this.row(index)?.focus();\n }\n\n private onOpen = (): void => {\n void this.moveTo(firstEnabledIndex(this.items));\n };\n\n /**\n * Returns focus to the caret, but only when it was inside the menu — an\n * outside click already moved it somewhere the user chose, and pulling it\n * back here would undo that.\n */\n private onClose = (): void => {\n const active = this.shadowRoot?.activeElement;\n this.activeIndex = -1;\n if (active instanceof HTMLElement && active.classList.contains('item')) this.caret?.focus();\n };\n\n private onMenuKeyDown = (event: KeyboardEvent): void => {\n const moves: Record<string, () => number> = {\n ArrowDown: () => nextEnabledIndex(this.items, this.activeIndex, 1),\n ArrowUp: () => nextEnabledIndex(this.items, this.activeIndex, -1),\n Home: () => firstEnabledIndex(this.items),\n End: () => lastEnabledIndex(this.items),\n };\n\n const move = moves[event.key];\n if (!move) return;\n\n event.preventDefault();\n void this.moveTo(move());\n };\n\n private choose(item: KtSplitButtonItem): void {\n if (item.disabled) return;\n this.hide();\n emit(this, 'kt-select', { value: item.id, item });\n }\n\n /**\n * A click anywhere in the menu stops here. It originates inside this\n * element's shadow root, so without this it would surface on the host as a\n * `click` and be mistaken for the primary action.\n */\n private containClick(event: Event): void {\n event.stopPropagation();\n }\n\n /**\n * The caret drives the panel itself rather than leaning on the click\n * reaching `<kt-dropdown>`'s own trigger through the slot: stopping here\n * keeps the press off the host, and leaving it to bubble would toggle twice.\n */\n private onCaretClick(event: Event): void {\n event.stopPropagation();\n this.toggle();\n }\n\n private renderItem(item: KtSplitButtonItem, index: number): TemplateResult {\n return html`<button\n part=\"item\"\n class=${classMap({ item: true, danger: item.variant === 'danger' })}\n type=\"button\"\n role=\"menuitem\"\n tabindex=${index === this.activeIndex ? 0 : -1}\n aria-disabled=${item.disabled ? 'true' : nothing}\n @click=${() => this.choose(item)}\n >\n ${\n item.icon\n ? html`<kt-icon name=${item.icon} size=${ICON_SIZE[this.size]}></kt-icon>`\n : nothing\n }\n ${optionLabel(item)}\n </button>`;\n }\n\n override render(): TemplateResult {\n return html`<div class=\"split\">\n <!-- The icon is slotted rather than passed as \\`icon\\`: <kt-button> reads\n its own text content to decide whether it is icon-only, and a <slot>\n has none, so setting \\`icon\\` here would collapse it to a square. -->\n <kt-button\n part=\"action\"\n class=\"action\"\n variant=${this.variant}\n size=${this.size}\n ?disabled=${this.disabled}\n >\n ${\n this.icon\n ? html`<kt-icon name=${this.icon} size=${ICON_SIZE[this.size]}></kt-icon>`\n : nothing\n }\n <slot></slot>\n </kt-button>\n\n <kt-dropdown\n align=\"end\"\n preferred-placement=${this.preferredPlacement}\n ?disabled=${this.disabled}\n @click=${this.containClick}\n @kt-open=${this.onOpen}\n @kt-close=${this.onClose}\n >\n <kt-button\n slot=\"trigger\"\n part=\"caret\"\n class=\"caret\"\n variant=${this.variant}\n size=${this.size}\n icon=\"chevron-down\"\n label=${this.menuLabel}\n ?disabled=${this.disabled}\n @click=${this.onCaretClick}\n ></kt-button>\n\n <div slot=\"panel\" part=\"menu\" class=\"menu\" role=\"menu\" @keydown=${this.onMenuKeyDown}>\n ${\n this.items.length > 0\n ? this.items.map((item, index) => this.renderItem(item, index))\n : html`<div class=\"empty\">${this.emptyText}</div>`\n }\n </div>\n </kt-dropdown>\n </div>`;\n }\n}\n\ndefineElement('kt-split-button', KtSplitButton);\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'kt-split-button': KtSplitButton;\n }\n}\n"],"names":["ICON_SIZE","KtSplitButton","KtElement","firstEnabledIndex","active","event","move","nextEnabledIndex","lastEnabledIndex","css","options","index","item","emit","html","classMap","nothing","optionLabel","__decorateClass","query","state","property","defineElement"],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import { type SVGTemplateResult } from 'lit';
2
+ import type { Format, KtMark, KtSeries, Measure, PlotBox } from './model.js';
3
+ import { type Ticks } from './scale.js';
4
+ /**
5
+ * Line, area and bar charts: one category axis, one value axis.
6
+ *
7
+ * The layout is computed once per render and every position is read from it,
8
+ * so the marks, the labels, the tooltip and the hit test cannot disagree about
9
+ * where a category is.
10
+ */
11
+ export interface CartesianInput {
12
+ readonly width: number;
13
+ readonly height: number;
14
+ readonly series: readonly KtSeries[];
15
+ readonly hidden: ReadonlySet<number>;
16
+ readonly labels: readonly string[];
17
+ /** The chart's mark; a series can override it with its own `type`. */
18
+ readonly type: KtMark;
19
+ readonly stacked: boolean;
20
+ readonly horizontal: boolean;
21
+ readonly min: number | null;
22
+ readonly max: number | null;
23
+ readonly noAxis: boolean;
24
+ readonly measure: Measure;
25
+ readonly format: Format;
26
+ }
27
+ export interface CartesianLayout {
28
+ readonly box: PlotBox;
29
+ readonly ticks: Ticks;
30
+ /** How many categories there are. */
31
+ readonly count: number;
32
+ readonly horizontal: boolean;
33
+ /** Categories are bands rather than points — true as soon as a bar is shown. */
34
+ readonly banded: boolean;
35
+ /** The mark each series is drawn as, by series index. */
36
+ readonly marks: readonly KtMark[];
37
+ /** Indices of the series that are not hidden. */
38
+ readonly visible: readonly number[];
39
+ }
40
+ export declare function cartesianLayout(input: CartesianInput): CartesianLayout;
41
+ /** Where category `index` sits along its axis, in px. */
42
+ export declare function categoryCentre(layout: CartesianLayout, index: number): number;
43
+ /** Where `value` sits along the value axis, in px. */
44
+ export declare function valuePosition(layout: CartesianLayout, value: number): number;
45
+ /** The category under a pointer at `x`, `y`: the band it is in, or the nearest sample. */
46
+ export declare function categoryAt(layout: CartesianLayout, x: number, y: number): number;
47
+ export interface CartesianView extends CartesianInput {
48
+ readonly layout: CartesianLayout;
49
+ /** The active category, or -1. */
50
+ readonly active: number;
51
+ readonly smooth: boolean;
52
+ readonly noGrid: boolean;
53
+ readonly colorOf: (series: number) => string;
54
+ /** Prefix for ids inside the shadow root, so two charts never share a gradient. */
55
+ readonly idPrefix: string;
56
+ }
57
+ export declare function renderCartesian(view: CartesianView): SVGTemplateResult;
58
+ /** Where the tooltip for the active category points: at its tallest mark. */
59
+ export declare function tooltipAnchor(view: CartesianView): {
60
+ x: number;
61
+ y: number;
62
+ };