@adea-ai/ui 0.81.0 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +41 -8
  2. package/dist/components/composites/account-menu/account-menu.d.ts +5 -0
  3. package/dist/components/composites/account-menu/account-menu.d.ts.map +1 -1
  4. package/dist/components/composites/account-menu/account-menu.js +122 -70
  5. package/dist/components/composites/account-menu/account-menu.js.map +1 -1
  6. package/dist/components/composites/action-button/action-button.d.ts +27 -0
  7. package/dist/components/composites/action-button/action-button.d.ts.map +1 -0
  8. package/dist/components/composites/action-button/action-button.js +121 -0
  9. package/dist/components/composites/action-button/action-button.js.map +1 -0
  10. package/dist/components/composites/action-button/index.d.ts +3 -0
  11. package/dist/components/composites/action-button/index.d.ts.map +1 -0
  12. package/dist/components/composites/action-button/index.js +2 -0
  13. package/dist/components/conversation/thread-panel.d.ts +2 -0
  14. package/dist/components/conversation/thread-panel.d.ts.map +1 -1
  15. package/dist/components/conversation/thread-panel.js +36 -31
  16. package/dist/components/conversation/thread-panel.js.map +1 -1
  17. package/dist/components/layout/sidebar-nav/index.d.ts +1 -1
  18. package/dist/components/layout/sidebar-nav/index.d.ts.map +1 -1
  19. package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts +6 -1
  20. package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts.map +1 -1
  21. package/dist/components/layout/sidebar-nav/sidebar-nav.js +66 -62
  22. package/dist/components/layout/sidebar-nav/sidebar-nav.js.map +1 -1
  23. package/dist/components/ui/board/board.d.ts.map +1 -1
  24. package/dist/components/ui/board/board.js +54 -7
  25. package/dist/components/ui/board/board.js.map +1 -1
  26. package/dist/components/ui/command/command.d.ts +5 -1
  27. package/dist/components/ui/command/command.d.ts.map +1 -1
  28. package/dist/components/ui/command/command.js +8 -3
  29. package/dist/components/ui/command/command.js.map +1 -1
  30. package/dist/components/ui/input/index.d.ts +1 -1
  31. package/dist/components/ui/input/index.d.ts.map +1 -1
  32. package/dist/components/ui/input/input.d.ts +5 -1
  33. package/dist/components/ui/input/input.d.ts.map +1 -1
  34. package/dist/components/ui/input/input.js +36 -17
  35. package/dist/components/ui/input/input.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +2 -1
  39. package/dist/lint/index.d.ts.map +1 -1
  40. package/dist/lint.js +47 -7
  41. package/dist/lint.js.map +1 -1
  42. package/dist/r/account-menu.json +1 -0
  43. package/dist/r/action-button.json +24 -0
  44. package/dist/r/conversation.json +1 -0
  45. package/dist/r/registry.json +26 -0
  46. package/dist/r/src/components/composites/account-menu/account-menu.tsx +100 -44
  47. package/dist/r/src/components/composites/action-button/action-button.tsx +139 -0
  48. package/dist/r/src/components/composites/action-button/index.ts +2 -0
  49. package/dist/r/src/components/conversation/thread-panel.tsx +22 -12
  50. package/dist/r/src/components/layout/sidebar-nav/index.ts +1 -0
  51. package/dist/r/src/components/layout/sidebar-nav/sidebar-nav.tsx +29 -21
  52. package/dist/r/src/components/ui/board/board.tsx +74 -6
  53. package/dist/r/src/components/ui/command/command.tsx +15 -4
  54. package/dist/r/src/components/ui/input/index.ts +1 -1
  55. package/dist/r/src/components/ui/input/input.tsx +38 -21
  56. package/package.json +1 -1
  57. package/registry.json +26 -0
  58. package/src/components/composites/account-menu/account-menu.tsx +100 -44
  59. package/src/components/composites/action-button/action-button.tsx +139 -0
  60. package/src/components/composites/action-button/index.ts +2 -0
  61. package/src/components/conversation/thread-panel.tsx +22 -12
  62. package/src/components/layout/sidebar-nav/index.ts +1 -0
  63. package/src/components/layout/sidebar-nav/sidebar-nav.tsx +29 -21
  64. package/src/components/ui/board/board.tsx +74 -6
  65. package/src/components/ui/command/command.tsx +15 -4
  66. package/src/components/ui/input/index.ts +1 -1
  67. package/src/components/ui/input/input.tsx +38 -21
  68. package/src/index.ts +1 -0
  69. package/src/lint/index.ts +71 -7
package/dist/lint.js CHANGED
@@ -69,11 +69,14 @@ var noRawInteractiveElements = {
69
69
  };
70
70
  var DEFAULT_PRIMITIVE_PACKAGES = Object.freeze([
71
71
  "@kobalte/core",
72
+ "@corvu",
73
+ "cmdk-solid",
72
74
  "@ark-ui",
73
75
  "@zag-js",
74
76
  "@radix-ui",
75
77
  "radix-ui",
76
- "@base-ui-components"
78
+ "@base-ui-components",
79
+ "@base-ui"
77
80
  ]);
78
81
  /**
79
82
  * The interactive ARIA roles an application can dress a generic element in, and
@@ -144,18 +147,44 @@ var GENERIC_ELEMENTS = /* @__PURE__ */ new Set([
144
147
  "img",
145
148
  "svg"
146
149
  ]);
147
- var CLICK_HANDLERS = /* @__PURE__ */ new Set(["onClick", "onDoubleClick"]);
148
- /** Reads a static `role` value; a dynamic role is undecidable statically, so it is left to review. */
150
+ var CLICK_HANDLERS = /* @__PURE__ */ new Set([
151
+ "onClick",
152
+ "onDoubleClick",
153
+ "onPointerDown",
154
+ "onPointerUp",
155
+ "onMouseDown",
156
+ "onMouseUp",
157
+ "on:click",
158
+ "on:dblclick",
159
+ "on:pointerdown",
160
+ "on:pointerup",
161
+ "on:mousedown",
162
+ "on:mouseup"
163
+ ]);
164
+ /** Only statically knowable branches; unresolved roles remain a review boundary. */
165
+ var staticStrings = (node) => {
166
+ if (node?.type === "Literal" && typeof node.value === "string") return [node.value];
167
+ if (node?.type === "JSXExpressionContainer") return staticStrings(node.expression);
168
+ if (node?.type === "ConditionalExpression") return [...staticStrings(node.consequent), ...staticStrings(node.alternate)];
169
+ if (node?.type === "TemplateLiteral" && node.expressions.length === 0) return [node.quasis[0].value.cooked ?? node.quasis[0].value.raw];
170
+ return [];
171
+ };
149
172
  var roleOf = (node) => {
150
173
  for (const attribute of node.attributes ?? []) {
151
174
  if (attribute.type !== "JSXAttribute" || attribute.name?.name !== "role") continue;
152
- if (attribute.value?.type === "Literal" && typeof attribute.value.value === "string") return attribute.value.value;
153
- return null;
175
+ return staticStrings(attribute.value).flatMap((role) => role.split(/\s+/)).find((role) => role in ROLE_PRIMITIVES) ?? null;
154
176
  }
155
177
  return null;
156
178
  };
157
179
  /** Whether the element carries the attribute, by name. */
158
- var has = (node, name) => (node.attributes ?? []).some((attribute) => attribute.type === "JSXAttribute" && attribute.name?.name === name);
180
+ var has = (node, name) => (node.attributes ?? []).some((attribute) => attribute.type === "JSXAttribute" && (attribute.name?.type === "JSXNamespacedName" ? `${attribute.name.namespace.name}:${attribute.name.name.name}` : attribute.name?.name) === name);
181
+ var hasTabStop = (node) => (node.attributes ?? []).some((attribute) => {
182
+ if (attribute.type !== "JSXAttribute" || !["tabIndex", "tabindex"].includes(attribute.name?.name)) return false;
183
+ const value = attribute.value?.type === "JSXExpressionContainer" ? attribute.value.expression : attribute.value;
184
+ if (value?.type === "Literal") return !(Number(value.value) < 0);
185
+ if (value?.type === "UnaryExpression" && value.operator === "-" && value.argument?.type === "Literal") return !(Number(value.argument.value) > 0);
186
+ return true;
187
+ });
159
188
  var plugin = {
160
189
  meta: { name: "adea" },
161
190
  rules: {
@@ -199,6 +228,17 @@ var plugin = {
199
228
  },
200
229
  ImportExpression(node) {
201
230
  if (typeof node.source?.value === "string" && offender(node.source.value)) report(node, node.source.value);
231
+ },
232
+ ExportNamedDeclaration(node) {
233
+ if (offender(node.source?.value)) report(node, node.source.value);
234
+ },
235
+ ExportAllDeclaration(node) {
236
+ if (offender(node.source?.value)) report(node, node.source.value);
237
+ },
238
+ CallExpression(node) {
239
+ if (node.callee?.type !== "Identifier" || node.callee.name !== "require") return;
240
+ const source = node.arguments?.[0]?.value;
241
+ if (typeof source === "string" && offender(source)) report(node, source);
202
242
  }
203
243
  };
204
244
  }
@@ -263,7 +303,7 @@ var plugin = {
263
303
  });
264
304
  return;
265
305
  }
266
- if (has(node, "tabIndex") || has(node, "tabindex")) context.report({
306
+ if (hasTabStop(node)) context.report({
267
307
  node,
268
308
  messageId: "tabindexWrapper",
269
309
  data: { element },
package/dist/lint.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"lint.js","names":[],"sources":["../src/lint/index.ts"],"sourcesContent":["/*\n * The design system's own oxlint plugin.\n *\n * The styling rules live in `@shadcn/lint`; what that plugin cannot know is which\n * *elements* the design system owns. This is the other half of the contract, for\n * the applications that consume the library: an interactive element this package\n * provides is not re-implemented at the application layer, and the primitive\n * libraries this package is built from are not imported around it.\n *\n * Loaded as an oxlint jsPlugin:\n *\n * // .oxlintrc.json\n * { \"jsPlugins\": [\"@adea-ai/ui/lint\"], \"rules\": { \"adea/no-raw-interactive-elements\": \"error\" } }\n *\n * Rules are errors by intent. A violation means an application composed its own\n * control out of raw markup — unreachable focus behaviour, an absent keyboard\n * story, a second implementation of a decision this package exists to make once.\n */\n\ntype Json = string | number | boolean | null | Json[] | { [key: string]: Json }\n\n/** The raw elements this package replaces, and the primitive that replaces them. */\nconst ELEMENT_PRIMITIVES: Readonly<Record<string, { primitive: string; path: string }>> =\n Object.freeze({\n button: { primitive: 'Button', path: '@adea-ai/ui/components/ui/button' },\n input: { primitive: 'Input', path: '@adea-ai/ui/components/ui/input' },\n textarea: { primitive: 'Textarea', path: '@adea-ai/ui/components/ui/textarea' },\n select: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },\n option: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },\n label: { primitive: 'Label', path: '@adea-ai/ui/components/ui/label' },\n })\n\ntype RuleModule = {\n meta: {\n type: 'problem'\n docs: { description: string }\n schema: Json[]\n messages: Record<string, string>\n }\n create: (context: LintContext) => Record<string, (node: any) => void>\n}\n\ninterface LintContext {\n options?: unknown[]\n report: (descriptor: {\n node: unknown\n messageId?: string\n message?: string\n data?: Record<string, Json>\n }) => void\n}\n\ninterface RawElementOptions {\n /** Element names this rule should not report (rare; prefer fixing the file). */\n allow?: string[]\n /** Replaces the default report message. */\n message?: string\n}\n\nconst noRawInteractiveElements: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Interactive elements this package provides (button, input, textarea, select, option, label) are composed from the package, not written as raw markup.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allow: { type: 'array', items: { type: 'string' } },\n message: { type: 'string' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n rawElement:\n '{{element}} is provided by the design system: use {{primitive}} from {{path}}. A raw element has no keyboard story, no focus behaviour and no token styling — that is the work the primitive already did.',\n },\n },\n create(context) {\n const options = (context.options?.[0] ?? {}) as RawElementOptions\n const allowed = new Set(options.allow ?? [])\n const emit = (node: unknown, element: string) => {\n const target = ELEMENT_PRIMITIVES[element]!\n context.report({\n node,\n messageId: 'rawElement',\n data: { element, primitive: target.primitive, path: target.path },\n ...(options.message ? { message: options.message } : {}),\n })\n }\n return {\n JSXOpeningElement(node: any) {\n if (node.name?.type !== 'JSXIdentifier') return\n const element = node.name.name\n if (!(element in ELEMENT_PRIMITIVES) || allowed.has(element)) return\n emit(node, element)\n },\n }\n },\n}\n\ninterface PrimitiveImportOptions {\n /** Package prefixes this rule reports. Defaults to the primitive libraries the ui package wraps. */\n packages?: string[]\n /** Replaces the default report message. */\n message?: string\n}\n\nconst DEFAULT_PRIMITIVE_PACKAGES = Object.freeze([\n '@kobalte/core',\n '@ark-ui',\n '@zag-js',\n '@radix-ui',\n 'radix-ui',\n '@base-ui-components',\n])\n\n/**\n * The interactive ARIA roles an application can dress a generic element in, and\n * the primitive that owns each role. `link` is deliberately absent: an `<a>` is\n * natively interactive and Button itself supports `as=\"a\"` for the link-styled\n * case.\n */\nconst ROLE_PRIMITIVES: Readonly<Record<string, { primitive: string; path: string }>> =\n Object.freeze({\n button: { primitive: 'Button', path: '@adea-ai/ui/components/ui/button' },\n checkbox: { primitive: 'Checkbox', path: '@adea-ai/ui/components/ui/checkbox' },\n radio: { primitive: 'RadioGroup', path: '@adea-ai/ui/components/ui/radio-group' },\n switch: { primitive: 'Switch', path: '@adea-ai/ui/components/ui/switch' },\n tab: { primitive: 'Tabs', path: '@adea-ai/ui/components/ui/tabs' },\n option: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },\n menuitem: { primitive: 'DropdownMenu', path: '@adea-ai/ui/components/ui/dropdown-menu' },\n textbox: { primitive: 'Input', path: '@adea-ai/ui/components/ui/input' },\n })\n\n/**\n * The elements whose only job is layout or text. A click handler or a tabindex on\n * one of these is a control being hand-built out of a non-control.\n */\nconst GENERIC_ELEMENTS: ReadonlySet<string> = new Set([\n 'div',\n 'span',\n 'li',\n 'ul',\n 'ol',\n 'td',\n 'th',\n 'p',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'section',\n 'article',\n 'aside',\n 'header',\n 'footer',\n 'nav',\n 'main',\n 'img',\n 'svg',\n])\n\nconst CLICK_HANDLERS: ReadonlySet<string> = new Set(['onClick', 'onDoubleClick'])\n\ninterface InteractiveWrapperOptions {\n /** Element names this rule should not report (rare; prefer fixing the file). */\n allow?: string[]\n /** Replaces the default report message. */\n message?: string\n}\n\n/** Reads a static `role` value; a dynamic role is undecidable statically, so it is left to review. */\nconst roleOf = (node: any): string | null => {\n for (const attribute of node.attributes ?? []) {\n if (attribute.type !== 'JSXAttribute' || attribute.name?.name !== 'role') continue\n if (attribute.value?.type === 'Literal' && typeof attribute.value.value === 'string')\n return attribute.value.value\n return null\n }\n return null\n}\n\n/** Whether the element carries the attribute, by name. */\nconst has = (node: any, name: string): boolean =>\n (node.attributes ?? []).some(\n (attribute: any) => attribute.type === 'JSXAttribute' && attribute.name?.name === name\n )\n\nconst noInteractiveWrappers: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Generic elements dressed as interactive controls — an interactive role, a click handler or a tabindex on a layout element — re-implement a primitive by hand. Compose the real component.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allow: { type: 'array', items: { type: 'string' } },\n message: { type: 'string' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n roleWrapper:\n 'A {{element}} with role=\"{{role}}\" hand-builds {{primitive}}: use {{primitive}} from {{path}}. The primitive brings keyboard activation, focus behaviour and ARIA wiring; the wrapper re-implements them badly.',\n clickWrapper:\n 'A click handler on {{element}} composes {{primitive}} by hand: use {{primitive}} from {{path}}. A div that reacts to clicks is a button that cannot be reached by keyboard.',\n tabindexWrapper:\n 'A tabindex on {{element}} makes a layout element focusable, which is focus management the primitives already own: compose the real component instead.',\n },\n },\n create(context) {\n const options = (context.options?.[0] ?? {}) as InteractiveWrapperOptions\n const allowed = new Set(options.allow ?? [])\n return {\n JSXOpeningElement(node: any) {\n if (node.name?.type !== 'JSXIdentifier') return\n const element = node.name.name\n if (allowed.has(element) || element in ELEMENT_PRIMITIVES) return\n // A capitalized element is a component — often a design-system primitive\n // composing a role deliberately (a Button carrying role=\"option\" inside a\n // custom listbox, for example). That is composition, not impersonation.\n if (element[0] === element[0]?.toUpperCase()) return\n\n const role = roleOf(node)\n if (role && role in ROLE_PRIMITIVES) {\n const target = ROLE_PRIMITIVES[role]!\n context.report({\n node,\n messageId: 'roleWrapper',\n data: { element, role, primitive: target.primitive, path: target.path },\n ...(options.message ? { message: options.message } : {}),\n })\n return\n }\n\n if (GENERIC_ELEMENTS.has(element)) {\n const click = [...CLICK_HANDLERS].find((handler) => has(node, handler))\n if (click) {\n const target = ROLE_PRIMITIVES.button!\n context.report({\n node,\n messageId: 'clickWrapper',\n data: { element, primitive: target.primitive, path: target.path },\n ...(options.message ? { message: options.message } : {}),\n })\n return\n }\n if (has(node, 'tabIndex') || has(node, 'tabindex')) {\n context.report({\n node,\n messageId: 'tabindexWrapper',\n data: { element },\n ...(options.message ? { message: options.message } : {}),\n })\n }\n }\n },\n }\n },\n}\n\nconst noPrimitiveLibraryImports: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'The primitive libraries the design system is built on (Kobalte and friends) are its internal affair — compose the exported components instead of importing the primitives.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n packages: { type: 'array', items: { type: 'string' } },\n message: { type: 'string' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n primitiveImport:\n 'Import from the design system, not from {{package}}: the primitives it wraps carry the focus, keyboard and ARIA behaviour the components exist to provide.',\n },\n },\n create(context) {\n const options = (context.options?.[0] ?? {}) as PrimitiveImportOptions\n const prefixes = options.packages ?? DEFAULT_PRIMITIVE_PACKAGES\n const offender = (source: unknown): string | null => {\n const name = String(source ?? '')\n return prefixes.find((prefix) => name === prefix || name.startsWith(`${prefix}/`)) ?? null\n }\n const report = (node: unknown, source: unknown) => {\n const pkg = offender(source)!\n context.report({\n node,\n messageId: 'primitiveImport',\n data: { package: pkg },\n ...(options.message ? { message: options.message } : {}),\n })\n }\n return {\n ImportDeclaration(node: any) {\n if (offender(node.source?.value)) report(node, node.source.value)\n },\n ImportExpression(node: any) {\n if (typeof node.source?.value === 'string' && offender(node.source.value))\n report(node, node.source.value)\n },\n }\n },\n}\n\nconst plugin = {\n meta: { name: 'adea' },\n rules: {\n 'no-raw-interactive-elements': noRawInteractiveElements,\n 'no-primitive-library-imports': noPrimitiveLibraryImports,\n 'no-interactive-wrappers': noInteractiveWrappers,\n },\n}\n\nexport default plugin\n"],"mappings":";;AAsBA,IAAM,qBACJ,OAAO,OAAO;CACZ,QAAQ;EAAE,WAAW;EAAU,MAAM;CAAmC;CACxE,OAAO;EAAE,WAAW;EAAS,MAAM;CAAkC;CACrE,UAAU;EAAE,WAAW;EAAY,MAAM;CAAqC;CAC9E,QAAQ;EAAE,WAAW;EAAU,MAAM;CAAmC;CACxE,QAAQ;EAAE,WAAW;EAAU,MAAM;CAAmC;CACxE,OAAO;EAAE,WAAW;EAAS,MAAM;CAAkC;AACvE,CAAC;AA6BH,IAAM,2BAAuC;CAC3C,MAAM;EACJ,MAAM;EACN,MAAM,EACJ,aACE,wJACJ;EACA,QAAQ,CACN;GACE,MAAM;GACN,YAAY;IACV,OAAO;KAAE,MAAM;KAAS,OAAO,EAAE,MAAM,SAAS;IAAE;IAClD,SAAS,EAAE,MAAM,SAAS;GAC5B;GACA,sBAAsB;EACxB,CACF;EACA,UAAU,EACR,YACE,4MACJ;CACF;CACA,OAAO,SAAS;EACd,MAAM,UAAW,QAAQ,UAAU,MAAM,CAAC;EAC1C,MAAM,UAAU,IAAI,IAAI,QAAQ,SAAS,CAAC,CAAC;EAC3C,MAAM,QAAQ,MAAe,YAAoB;GAC/C,MAAM,SAAS,mBAAmB;GAClC,QAAQ,OAAO;IACb;IACA,WAAW;IACX,MAAM;KAAE;KAAS,WAAW,OAAO;KAAW,MAAM,OAAO;IAAK;IAChE,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;GACxD,CAAC;EACH;EACA,OAAO,EACL,kBAAkB,MAAW;GAC3B,IAAI,KAAK,MAAM,SAAS,iBAAiB;GACzC,MAAM,UAAU,KAAK,KAAK;GAC1B,IAAI,EAAE,WAAW,uBAAuB,QAAQ,IAAI,OAAO,GAAG;GAC9D,KAAK,MAAM,OAAO;EACpB,EACF;CACF;AACF;AASA,IAAM,6BAA6B,OAAO,OAAO;CAC/C;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;AAQD,IAAM,kBACJ,OAAO,OAAO;CACZ,QAAQ;EAAE,WAAW;EAAU,MAAM;CAAmC;CACxE,UAAU;EAAE,WAAW;EAAY,MAAM;CAAqC;CAC9E,OAAO;EAAE,WAAW;EAAc,MAAM;CAAwC;CAChF,QAAQ;EAAE,WAAW;EAAU,MAAM;CAAmC;CACxE,KAAK;EAAE,WAAW;EAAQ,MAAM;CAAiC;CACjE,QAAQ;EAAE,WAAW;EAAU,MAAM;CAAmC;CACxE,UAAU;EAAE,WAAW;EAAgB,MAAM;CAA0C;CACvF,SAAS;EAAE,WAAW;EAAS,MAAM;CAAkC;AACzE,CAAC;;;;;AAMH,IAAM,mCAAwC,IAAI,IAAI;CACpD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAED,IAAM,iCAAsC,IAAI,IAAI,CAAC,WAAW,eAAe,CAAC;;AAUhF,IAAM,UAAU,SAA6B;CAC3C,KAAK,MAAM,aAAa,KAAK,cAAc,CAAC,GAAG;EAC7C,IAAI,UAAU,SAAS,kBAAkB,UAAU,MAAM,SAAS,QAAQ;EAC1E,IAAI,UAAU,OAAO,SAAS,aAAa,OAAO,UAAU,MAAM,UAAU,UAC1E,OAAO,UAAU,MAAM;EACzB,OAAO;CACT;CACA,OAAO;AACT;;AAGA,IAAM,OAAO,MAAW,UACrB,KAAK,cAAc,CAAC,EAAA,CAAG,MACrB,cAAmB,UAAU,SAAS,kBAAkB,UAAU,MAAM,SAAS,IACpF;AAiIF,IAAM,SAAS;CACb,MAAM,EAAE,MAAM,OAAO;CACrB,OAAO;EACL,+BAA+B;EAC/B,gCAAgC;GArDlC,MAAM;IACJ,MAAM;IACN,MAAM,EACJ,aACE,6KACJ;IACA,QAAQ,CACN;KACE,MAAM;KACN,YAAY;MACV,UAAU;OAAE,MAAM;OAAS,OAAO,EAAE,MAAM,SAAS;MAAE;MACrD,SAAS,EAAE,MAAM,SAAS;KAC5B;KACA,sBAAsB;IACxB,CACF;IACA,UAAU,EACR,iBACE,6JACJ;GACF;GACA,OAAO,SAAS;IACd,MAAM,UAAW,QAAQ,UAAU,MAAM,CAAC;IAC1C,MAAM,WAAW,QAAQ,YAAY;IACrC,MAAM,YAAY,WAAmC;KACnD,MAAM,OAAO,OAAO,UAAU,EAAE;KAChC,OAAO,SAAS,MAAM,WAAW,SAAS,UAAU,KAAK,WAAW,GAAG,OAAO,EAAE,CAAC,KAAK;IACxF;IACA,MAAM,UAAU,MAAe,WAAoB;KACjD,MAAM,MAAM,SAAS,MAAM;KAC3B,QAAQ,OAAO;MACb;MACA,WAAW;MACX,MAAM,EAAE,SAAS,IAAI;MACrB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;KACxD,CAAC;IACH;IACA,OAAO;KACL,kBAAkB,MAAW;MAC3B,IAAI,SAAS,KAAK,QAAQ,KAAK,GAAG,OAAO,MAAM,KAAK,OAAO,KAAK;KAClE;KACA,iBAAiB,MAAW;MAC1B,IAAI,OAAO,KAAK,QAAQ,UAAU,YAAY,SAAS,KAAK,OAAO,KAAK,GACtE,OAAO,MAAM,KAAK,OAAO,KAAK;KAClC;IACF;GACF;EAOkC;EAChC,2BAA2B;GAnI7B,MAAM;IACJ,MAAM;IACN,MAAM,EACJ,aACE,4LACJ;IACA,QAAQ,CACN;KACE,MAAM;KACN,YAAY;MACV,OAAO;OAAE,MAAM;OAAS,OAAO,EAAE,MAAM,SAAS;MAAE;MAClD,SAAS,EAAE,MAAM,SAAS;KAC5B;KACA,sBAAsB;IACxB,CACF;IACA,UAAU;KACR,aACE;KACF,cACE;KACF,iBACE;IACJ;GACF;GACA,OAAO,SAAS;IACd,MAAM,UAAW,QAAQ,UAAU,MAAM,CAAC;IAC1C,MAAM,UAAU,IAAI,IAAI,QAAQ,SAAS,CAAC,CAAC;IAC3C,OAAO,EACL,kBAAkB,MAAW;KAC3B,IAAI,KAAK,MAAM,SAAS,iBAAiB;KACzC,MAAM,UAAU,KAAK,KAAK;KAC1B,IAAI,QAAQ,IAAI,OAAO,KAAK,WAAW,oBAAoB;KAI3D,IAAI,QAAQ,OAAO,QAAQ,EAAE,EAAE,YAAY,GAAG;KAE9C,MAAM,OAAO,OAAO,IAAI;KACxB,IAAI,QAAQ,QAAQ,iBAAiB;MACnC,MAAM,SAAS,gBAAgB;MAC/B,QAAQ,OAAO;OACb;OACA,WAAW;OACX,MAAM;QAAE;QAAS;QAAM,WAAW,OAAO;QAAW,MAAM,OAAO;OAAK;OACtE,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;MACxD,CAAC;MACD;KACF;KAEA,IAAI,iBAAiB,IAAI,OAAO,GAAG;MAEjC,IADc,CAAC,GAAG,cAAc,CAAC,CAAC,MAAM,YAAY,IAAI,MAAM,OAAO,CACjE,GAAO;OACT,MAAM,SAAS,gBAAgB;OAC/B,QAAQ,OAAO;QACb;QACA,WAAW;QACX,MAAM;SAAE;SAAS,WAAW,OAAO;SAAW,MAAM,OAAO;QAAK;QAChE,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;OACxD,CAAC;OACD;MACF;MACA,IAAI,IAAI,MAAM,UAAU,KAAK,IAAI,MAAM,UAAU,GAC/C,QAAQ,OAAO;OACb;OACA,WAAW;OACX,MAAM,EAAE,QAAQ;OAChB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;MACxD,CAAC;KAEL;IACF,EACF;GACF;EA0D6B;CAC7B;AACF"}
1
+ {"version":3,"file":"lint.js","names":[],"sources":["../src/lint/index.ts"],"sourcesContent":["/*\n * The design system's own oxlint plugin.\n *\n * The styling rules live in `@shadcn/lint`; what that plugin cannot know is which\n * *elements* the design system owns. This is the other half of the contract, for\n * the applications that consume the library: an interactive element this package\n * provides is not re-implemented at the application layer, and the primitive\n * libraries this package is built from are not imported around it.\n *\n * Loaded as an oxlint jsPlugin:\n *\n * // .oxlintrc.json\n * { \"jsPlugins\": [\"@adea-ai/ui/lint\"], \"rules\": { \"adea/no-raw-interactive-elements\": \"error\" } }\n *\n * Rules are errors by intent. A violation means an application composed its own\n * control out of raw markup — unreachable focus behaviour, an absent keyboard\n * story, a second implementation of a decision this package exists to make once.\n */\n\ntype Json = string | number | boolean | null | Json[] | { [key: string]: Json }\n\n/** The raw elements this package replaces, and the primitive that replaces them. */\nconst ELEMENT_PRIMITIVES: Readonly<Record<string, { primitive: string; path: string }>> =\n Object.freeze({\n button: { primitive: 'Button', path: '@adea-ai/ui/components/ui/button' },\n input: { primitive: 'Input', path: '@adea-ai/ui/components/ui/input' },\n textarea: { primitive: 'Textarea', path: '@adea-ai/ui/components/ui/textarea' },\n select: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },\n option: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },\n label: { primitive: 'Label', path: '@adea-ai/ui/components/ui/label' },\n })\n\ntype RuleModule = {\n meta: {\n type: 'problem'\n docs: { description: string }\n schema: Json[]\n messages: Record<string, string>\n }\n create: (context: LintContext) => Record<string, (node: any) => void>\n}\n\ninterface LintContext {\n options?: unknown[]\n report: (descriptor: {\n node: unknown\n messageId?: string\n message?: string\n data?: Record<string, Json>\n }) => void\n}\n\ninterface RawElementOptions {\n /** Element names this rule should not report (rare; prefer fixing the file). */\n allow?: string[]\n /** Replaces the default report message. */\n message?: string\n}\n\nconst noRawInteractiveElements: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Interactive elements this package provides (button, input, textarea, select, option, label) are composed from the package, not written as raw markup.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allow: { type: 'array', items: { type: 'string' } },\n message: { type: 'string' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n rawElement:\n '{{element}} is provided by the design system: use {{primitive}} from {{path}}. A raw element has no keyboard story, no focus behaviour and no token styling — that is the work the primitive already did.',\n },\n },\n create(context) {\n const options = (context.options?.[0] ?? {}) as RawElementOptions\n const allowed = new Set(options.allow ?? [])\n const emit = (node: unknown, element: string) => {\n const target = ELEMENT_PRIMITIVES[element]!\n context.report({\n node,\n messageId: 'rawElement',\n data: { element, primitive: target.primitive, path: target.path },\n ...(options.message ? { message: options.message } : {}),\n })\n }\n return {\n JSXOpeningElement(node: any) {\n if (node.name?.type !== 'JSXIdentifier') return\n const element = node.name.name\n if (!(element in ELEMENT_PRIMITIVES) || allowed.has(element)) return\n emit(node, element)\n },\n }\n },\n}\n\ninterface PrimitiveImportOptions {\n /** Package prefixes this rule reports. Defaults to the primitive libraries the ui package wraps. */\n packages?: string[]\n /** Replaces the default report message. */\n message?: string\n}\n\nconst DEFAULT_PRIMITIVE_PACKAGES = Object.freeze([\n '@kobalte/core',\n '@corvu',\n 'cmdk-solid',\n '@ark-ui',\n '@zag-js',\n '@radix-ui',\n 'radix-ui',\n '@base-ui-components',\n '@base-ui',\n])\n\n/**\n * The interactive ARIA roles an application can dress a generic element in, and\n * the primitive that owns each role. `link` is deliberately absent: an `<a>` is\n * natively interactive and Button itself supports `as=\"a\"` for the link-styled\n * case.\n */\nconst ROLE_PRIMITIVES: Readonly<Record<string, { primitive: string; path: string }>> =\n Object.freeze({\n button: { primitive: 'Button', path: '@adea-ai/ui/components/ui/button' },\n checkbox: { primitive: 'Checkbox', path: '@adea-ai/ui/components/ui/checkbox' },\n radio: { primitive: 'RadioGroup', path: '@adea-ai/ui/components/ui/radio-group' },\n switch: { primitive: 'Switch', path: '@adea-ai/ui/components/ui/switch' },\n tab: { primitive: 'Tabs', path: '@adea-ai/ui/components/ui/tabs' },\n option: { primitive: 'Select', path: '@adea-ai/ui/components/ui/select' },\n menuitem: { primitive: 'DropdownMenu', path: '@adea-ai/ui/components/ui/dropdown-menu' },\n textbox: { primitive: 'Input', path: '@adea-ai/ui/components/ui/input' },\n })\n\n/**\n * The elements whose only job is layout or text. A click handler or a tabindex on\n * one of these is a control being hand-built out of a non-control.\n */\nconst GENERIC_ELEMENTS: ReadonlySet<string> = new Set([\n 'div',\n 'span',\n 'li',\n 'ul',\n 'ol',\n 'td',\n 'th',\n 'p',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'section',\n 'article',\n 'aside',\n 'header',\n 'footer',\n 'nav',\n 'main',\n 'img',\n 'svg',\n])\n\nconst CLICK_HANDLERS: ReadonlySet<string> = new Set([\n 'onClick',\n 'onDoubleClick',\n 'onPointerDown',\n 'onPointerUp',\n 'onMouseDown',\n 'onMouseUp',\n 'on:click',\n 'on:dblclick',\n 'on:pointerdown',\n 'on:pointerup',\n 'on:mousedown',\n 'on:mouseup',\n])\n\ninterface InteractiveWrapperOptions {\n /** Element names this rule should not report (rare; prefer fixing the file). */\n allow?: string[]\n /** Replaces the default report message. */\n message?: string\n}\n\n/** Only statically knowable branches; unresolved roles remain a review boundary. */\nconst staticStrings = (node: any): string[] => {\n if (node?.type === 'Literal' && typeof node.value === 'string') return [node.value]\n if (node?.type === 'JSXExpressionContainer') return staticStrings(node.expression)\n if (node?.type === 'ConditionalExpression')\n return [...staticStrings(node.consequent), ...staticStrings(node.alternate)]\n if (node?.type === 'TemplateLiteral' && node.expressions.length === 0)\n return [node.quasis[0].value.cooked ?? node.quasis[0].value.raw]\n return []\n}\n\nconst roleOf = (node: any): string | null => {\n for (const attribute of node.attributes ?? []) {\n if (attribute.type !== 'JSXAttribute' || attribute.name?.name !== 'role') continue\n return (\n staticStrings(attribute.value)\n .flatMap((role) => role.split(/\\s+/))\n .find((role) => role in ROLE_PRIMITIVES) ?? null\n )\n }\n return null\n}\n\n/** Whether the element carries the attribute, by name. */\nconst has = (node: any, name: string): boolean =>\n (node.attributes ?? []).some(\n (attribute: any) =>\n attribute.type === 'JSXAttribute' &&\n (attribute.name?.type === 'JSXNamespacedName'\n ? `${attribute.name.namespace.name}:${attribute.name.name.name}`\n : attribute.name?.name) === name\n )\n\nconst hasTabStop = (node: any): boolean =>\n (node.attributes ?? []).some((attribute: any) => {\n if (\n attribute.type !== 'JSXAttribute' ||\n !['tabIndex', 'tabindex'].includes(attribute.name?.name)\n )\n return false\n const value =\n attribute.value?.type === 'JSXExpressionContainer'\n ? attribute.value.expression\n : attribute.value\n if (value?.type === 'Literal') return !(Number(value.value) < 0)\n if (\n value?.type === 'UnaryExpression' &&\n value.operator === '-' &&\n value.argument?.type === 'Literal'\n )\n return !(Number(value.argument.value) > 0)\n return true\n })\n\nconst noInteractiveWrappers: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Generic elements dressed as interactive controls — an interactive role, a click handler or a tabindex on a layout element — re-implement a primitive by hand. Compose the real component.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n allow: { type: 'array', items: { type: 'string' } },\n message: { type: 'string' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n roleWrapper:\n 'A {{element}} with role=\"{{role}}\" hand-builds {{primitive}}: use {{primitive}} from {{path}}. The primitive brings keyboard activation, focus behaviour and ARIA wiring; the wrapper re-implements them badly.',\n clickWrapper:\n 'A click handler on {{element}} composes {{primitive}} by hand: use {{primitive}} from {{path}}. A div that reacts to clicks is a button that cannot be reached by keyboard.',\n tabindexWrapper:\n 'A tabindex on {{element}} makes a layout element focusable, which is focus management the primitives already own: compose the real component instead.',\n },\n },\n create(context) {\n const options = (context.options?.[0] ?? {}) as InteractiveWrapperOptions\n const allowed = new Set(options.allow ?? [])\n return {\n JSXOpeningElement(node: any) {\n if (node.name?.type !== 'JSXIdentifier') return\n const element = node.name.name\n if (allowed.has(element) || element in ELEMENT_PRIMITIVES) return\n // A capitalized element is a component — often a design-system primitive\n // composing a role deliberately (a Button carrying role=\"option\" inside a\n // custom listbox, for example). That is composition, not impersonation.\n if (element[0] === element[0]?.toUpperCase()) return\n\n const role = roleOf(node)\n if (role && role in ROLE_PRIMITIVES) {\n const target = ROLE_PRIMITIVES[role]!\n context.report({\n node,\n messageId: 'roleWrapper',\n data: { element, role, primitive: target.primitive, path: target.path },\n ...(options.message ? { message: options.message } : {}),\n })\n return\n }\n\n if (GENERIC_ELEMENTS.has(element)) {\n const click = [...CLICK_HANDLERS].find((handler) => has(node, handler))\n if (click) {\n const target = ROLE_PRIMITIVES.button!\n context.report({\n node,\n messageId: 'clickWrapper',\n data: { element, primitive: target.primitive, path: target.path },\n ...(options.message ? { message: options.message } : {}),\n })\n return\n }\n if (hasTabStop(node)) {\n context.report({\n node,\n messageId: 'tabindexWrapper',\n data: { element },\n ...(options.message ? { message: options.message } : {}),\n })\n }\n }\n },\n }\n },\n}\n\nconst noPrimitiveLibraryImports: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'The primitive libraries the design system is built on (Kobalte and friends) are its internal affair — compose the exported components instead of importing the primitives.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n packages: { type: 'array', items: { type: 'string' } },\n message: { type: 'string' },\n },\n additionalProperties: false,\n },\n ],\n messages: {\n primitiveImport:\n 'Import from the design system, not from {{package}}: the primitives it wraps carry the focus, keyboard and ARIA behaviour the components exist to provide.',\n },\n },\n create(context) {\n const options = (context.options?.[0] ?? {}) as PrimitiveImportOptions\n const prefixes = options.packages ?? DEFAULT_PRIMITIVE_PACKAGES\n const offender = (source: unknown): string | null => {\n const name = String(source ?? '')\n return prefixes.find((prefix) => name === prefix || name.startsWith(`${prefix}/`)) ?? null\n }\n const report = (node: unknown, source: unknown) => {\n const pkg = offender(source)!\n context.report({\n node,\n messageId: 'primitiveImport',\n data: { package: pkg },\n ...(options.message ? { message: options.message } : {}),\n })\n }\n return {\n ImportDeclaration(node: any) {\n if (offender(node.source?.value)) report(node, node.source.value)\n },\n ImportExpression(node: any) {\n if (typeof node.source?.value === 'string' && offender(node.source.value))\n report(node, node.source.value)\n },\n ExportNamedDeclaration(node: any) {\n if (offender(node.source?.value)) report(node, node.source.value)\n },\n ExportAllDeclaration(node: any) {\n if (offender(node.source?.value)) report(node, node.source.value)\n },\n CallExpression(node: any) {\n if (node.callee?.type !== 'Identifier' || node.callee.name !== 'require') return\n const source = node.arguments?.[0]?.value\n if (typeof source === 'string' && offender(source)) report(node, source)\n },\n }\n },\n}\n\nconst plugin = {\n meta: { name: 'adea' },\n rules: {\n 'no-raw-interactive-elements': noRawInteractiveElements,\n 'no-primitive-library-imports': noPrimitiveLibraryImports,\n 'no-interactive-wrappers': noInteractiveWrappers,\n },\n}\n\nexport default plugin\n"],"mappings":"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"}
@@ -9,6 +9,7 @@
9
9
  "registryDependencies": [
10
10
  "@adea-ai/ui/button",
11
11
  "@adea-ai/ui/dropdown-menu",
12
+ "@adea-ai/ui/tooltip",
12
13
  "lib"
13
14
  ],
14
15
  "files": [
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "action-button",
3
+ "type": "registry:ui",
4
+ "title": "Action Button",
5
+ "description": "A shared Button with optional tooltip and busy-state feedback.",
6
+ "dependencies": [],
7
+ "registryDependencies": [
8
+ "@adea-ai/ui/button",
9
+ "@adea-ai/ui/spinner",
10
+ "@adea-ai/ui/tooltip"
11
+ ],
12
+ "files": [
13
+ {
14
+ "path": "src/components/composites/action-button/action-button.tsx",
15
+ "type": "registry:ui",
16
+ "target": "components/composites/action-button/action-button.tsx"
17
+ },
18
+ {
19
+ "path": "src/components/composites/action-button/index.ts",
20
+ "type": "registry:ui",
21
+ "target": "components/composites/action-button/index.ts"
22
+ }
23
+ ]
24
+ }
@@ -15,6 +15,7 @@
15
15
  "@adea-ai/ui/kbd",
16
16
  "@adea-ai/ui/spinner",
17
17
  "@adea-ai/ui/textarea",
18
+ "@adea-ai/ui/tooltip",
18
19
  "lib"
19
20
  ],
20
21
  "files": [
@@ -1649,6 +1649,7 @@
1649
1649
  "registryDependencies": [
1650
1650
  "@adea-ai/ui/button",
1651
1651
  "@adea-ai/ui/dropdown-menu",
1652
+ "@adea-ai/ui/tooltip",
1652
1653
  "lib"
1653
1654
  ],
1654
1655
  "files": [
@@ -1664,6 +1665,30 @@
1664
1665
  }
1665
1666
  ]
1666
1667
  },
1668
+ {
1669
+ "name": "action-button",
1670
+ "type": "registry:ui",
1671
+ "title": "Action Button",
1672
+ "description": "A shared Button with optional tooltip and busy-state feedback.",
1673
+ "dependencies": [],
1674
+ "registryDependencies": [
1675
+ "@adea-ai/ui/button",
1676
+ "@adea-ai/ui/spinner",
1677
+ "@adea-ai/ui/tooltip"
1678
+ ],
1679
+ "files": [
1680
+ {
1681
+ "path": "src/components/composites/action-button/action-button.tsx",
1682
+ "type": "registry:ui",
1683
+ "target": "components/composites/action-button/action-button.tsx"
1684
+ },
1685
+ {
1686
+ "path": "src/components/composites/action-button/index.ts",
1687
+ "type": "registry:ui",
1688
+ "target": "components/composites/action-button/index.ts"
1689
+ }
1690
+ ]
1691
+ },
1667
1692
  {
1668
1693
  "name": "appearance-editor",
1669
1694
  "type": "registry:ui",
@@ -1853,6 +1878,7 @@
1853
1878
  "@adea-ai/ui/kbd",
1854
1879
  "@adea-ai/ui/spinner",
1855
1880
  "@adea-ai/ui/textarea",
1881
+ "@adea-ai/ui/tooltip",
1856
1882
  "lib"
1857
1883
  ],
1858
1884
  "files": [
@@ -9,10 +9,10 @@ import {
9
9
  Smartphone,
10
10
  UserRound,
11
11
  } from 'lucide-solid'
12
- import type { JSX } from 'solid-js'
13
- import { For, Show, splitProps } from 'solid-js'
12
+ import type { ComponentProps, JSX } from 'solid-js'
13
+ import { For, Show, createSignal, onCleanup, splitProps } from 'solid-js'
14
14
  import { cn } from '../../../lib/utils'
15
- import { Button } from '../../ui/button/button'
15
+ import { Button, type ButtonProps } from '../../ui/button/button'
16
16
  import {
17
17
  DropdownMenu,
18
18
  DropdownMenuContent,
@@ -21,6 +21,7 @@ import {
21
21
  DropdownMenuSeparator,
22
22
  DropdownMenuTrigger,
23
23
  } from '../../ui/dropdown-menu/dropdown-menu'
24
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip/tooltip'
24
25
 
25
26
  /**
26
27
  * AccountMenu.
@@ -49,10 +50,14 @@ export type AccountMenuItem = {
49
50
  /** Render only on this platform. Omit to always render. */
50
51
  platform?: 'desktop' | 'web'
51
52
  onSelect?: () => void
53
+ /** Run after the menu's close-focus cycle, with its trigger as a stable opener. */
54
+ onSelectAfterClose?: (trigger: HTMLButtonElement | undefined) => void
52
55
  }
53
56
 
54
57
  export type AccountMenuProps = {
55
58
  class?: string
59
+ /** The trigger size. Icon-only menus default to the standard icon size. */
60
+ size?: ButtonProps['size']
56
61
  /** The entries above the separator. */
57
62
  items: readonly AccountMenuItem[]
58
63
  /** Which platform this is rendered on, for items that declare one. */
@@ -81,6 +86,7 @@ const FALLBACK_ICONS: Record<string, typeof Settings2> = {
81
86
  export function AccountMenu(props: AccountMenuProps) {
82
87
  const [local, rest] = splitProps(props, [
83
88
  'class',
89
+ 'size',
84
90
  'items',
85
91
  'platform',
86
92
  'authenticated',
@@ -91,55 +97,105 @@ export function AccountMenu(props: AccountMenuProps) {
91
97
  'onIntent',
92
98
  ])
93
99
 
100
+ let trigger: HTMLButtonElement | undefined
101
+ let pendingAfterClose:
102
+ | { callback: NonNullable<AccountMenuItem['onSelectAfterClose']> }
103
+ | undefined
104
+ let scheduledAfterClose:
105
+ | { callback: NonNullable<AccountMenuItem['onSelectAfterClose']> }
106
+ | undefined
107
+ const [menuOpen, setMenuOpen] = createSignal(false)
108
+ const [tooltipOpen, setTooltipOpen] = createSignal(false)
109
+
110
+ onCleanup(() => {
111
+ pendingAfterClose = undefined
112
+ scheduledAfterClose = undefined
113
+ })
114
+
94
115
  const visible = () =>
95
116
  local.items.filter((item) => !item.platform || item.platform === (local.platform ?? 'desktop'))
96
117
 
97
118
  return (
98
- <DropdownMenu modal={false}>
99
- <DropdownMenuTrigger
100
- as={Button}
101
- variant="ghost"
102
- size="icon-md"
103
- class={cn('text-muted-foreground', local.class)}
104
- aria-label={local.label ?? 'Account and settings'}
105
- onFocus={() => local.onIntent?.()}
106
- onPointerEnter={() => local.onIntent?.()}
107
- {...rest}
119
+ <Tooltip open={tooltipOpen()} onOpenChange={(open) => setTooltipOpen(open && !menuOpen())}>
120
+ <DropdownMenu
121
+ modal={false}
122
+ onOpenChange={(open) => {
123
+ setMenuOpen(open)
124
+ if (open) setTooltipOpen(false)
125
+ }}
108
126
  >
109
- <UserRound aria-hidden="true" />
110
- </DropdownMenuTrigger>
111
- <DropdownMenuContent side="top" align="start" class="min-w-56">
112
- <DropdownMenuGroup>
113
- <For each={visible()}>
114
- {(item) => (
115
- <DropdownMenuItem
116
- disabled={item.disabled}
117
- onSelect={() => item.onSelect?.()}
118
- shortcut={item.shortcut}
119
- >
120
- {item.icon ?? renderFallbackIcon(item.id)}
121
- <span>{item.label}</span>
122
- </DropdownMenuItem>
123
- )}
124
- </For>
125
- </DropdownMenuGroup>
126
- <DropdownMenuSeparator />
127
- <DropdownMenuGroup>
128
- <DropdownMenuItem
129
- disabled={local.busy}
130
- onSelect={() => (local.authenticated ? local.onSignOut?.() : local.onSignIn?.())}
131
- >
132
- <Show when={local.authenticated} fallback={<LogIn aria-hidden="true" />}>
133
- <LogOut aria-hidden="true" />
134
- </Show>
135
- <span>{local.authenticated ? 'Sign out' : 'Sign in'}</span>
136
- </DropdownMenuItem>
137
- </DropdownMenuGroup>
138
- </DropdownMenuContent>
139
- </DropdownMenu>
127
+ <TooltipTrigger
128
+ as={AccountMenuButton}
129
+ ref={(element: HTMLButtonElement) => (trigger = element)}
130
+ variant="ghost"
131
+ size={local.size ?? 'icon-md'}
132
+ class={cn('text-muted-foreground', local.class)}
133
+ aria-label={local.label ?? 'Account and settings'}
134
+ onFocus={() => local.onIntent?.()}
135
+ onPointerEnter={() => local.onIntent?.()}
136
+ {...rest}
137
+ >
138
+ <UserRound aria-hidden="true" />
139
+ </TooltipTrigger>
140
+ <TooltipContent placement="top">{local.label ?? 'Account and settings'}</TooltipContent>
141
+ <DropdownMenuContent
142
+ placement="top-start"
143
+ class="min-w-56 max-h-(--kb-popper-available-height) overflow-x-hidden overflow-y-auto"
144
+ onCloseAutoFocus={(event) => {
145
+ const selection = pendingAfterClose
146
+ pendingAfterClose = undefined
147
+ if (!selection) return
148
+
149
+ event.preventDefault()
150
+ scheduledAfterClose = selection
151
+ queueMicrotask(() => {
152
+ if (scheduledAfterClose !== selection) return
153
+ scheduledAfterClose = undefined
154
+ selection.callback(trigger)
155
+ })
156
+ }}
157
+ >
158
+ <DropdownMenuGroup>
159
+ <For each={visible()}>
160
+ {(item) => (
161
+ <DropdownMenuItem
162
+ disabled={item.disabled}
163
+ onSelect={() => {
164
+ pendingAfterClose = item.onSelectAfterClose
165
+ ? { callback: item.onSelectAfterClose }
166
+ : undefined
167
+ item.onSelect?.()
168
+ }}
169
+ shortcut={item.shortcut}
170
+ >
171
+ {item.icon ?? renderFallbackIcon(item.id)}
172
+ <span>{item.label}</span>
173
+ </DropdownMenuItem>
174
+ )}
175
+ </For>
176
+ </DropdownMenuGroup>
177
+ <DropdownMenuSeparator />
178
+ <DropdownMenuGroup>
179
+ <DropdownMenuItem
180
+ disabled={local.busy}
181
+ onSelect={() => (local.authenticated ? local.onSignOut?.() : local.onSignIn?.())}
182
+ >
183
+ <Show when={local.authenticated} fallback={<LogIn aria-hidden="true" />}>
184
+ <LogOut aria-hidden="true" />
185
+ </Show>
186
+ <span>{local.authenticated ? 'Sign out' : 'Sign in'}</span>
187
+ </DropdownMenuItem>
188
+ </DropdownMenuGroup>
189
+ </DropdownMenuContent>
190
+ </DropdownMenu>
191
+ </Tooltip>
140
192
  )
141
193
  }
142
194
 
195
+ function AccountMenuButton(props: ComponentProps<typeof Button>) {
196
+ return <DropdownMenuTrigger as={Button} {...props} />
197
+ }
198
+
143
199
  /**
144
200
  * A known item id gets its conventional glyph, so a caller that only wants the
145
201
  * standard menu does not have to import six icons to get it.
@@ -0,0 +1,139 @@
1
+ import { Show, splitProps, type ComponentProps, type JSX, type ValidComponent } from 'solid-js'
2
+ import { Button, type ButtonProps } from '../../ui/button'
3
+ import { Spinner } from '../../ui/spinner'
4
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
5
+
6
+ /**
7
+ * ActionButton.
8
+ *
9
+ * A Button with optional explanatory tooltip and busy feedback. Use it for a
10
+ * simple action that needs these states; actions with product-specific rules
11
+ * should compose Button directly.
12
+ *
13
+ * The tooltip trigger renders the polymorphic Button itself. A disabled native
14
+ * control cannot receive pointer or keyboard events. When a tooltip is set,
15
+ * disabled actions therefore use focusable `aria-disabled` semantics and guard
16
+ * activation; without a tooltip, native buttons retain native disabling.
17
+ * Custom polymorphic components must forward aria, event, and href props for
18
+ * these guarantees to apply.
19
+ */
20
+ export type ActionButtonProps<T extends ValidComponent = 'button'> = ButtonProps<T> & {
21
+ /** A short explanation, also shown on keyboard focus. The Button still needs its own name. */
22
+ tooltip?: string
23
+ tooltipSide?: 'top' | 'right' | 'bottom' | 'left'
24
+ /** Disables the action and announces the busy label through a polite status region. */
25
+ busy?: boolean
26
+ /** The activity announced while busy. The Button's accessible name stays unchanged. */
27
+ busyLabel?: string
28
+ }
29
+
30
+ function guardedClick(handler: unknown, disabled: () => boolean) {
31
+ return (event: MouseEvent) => {
32
+ if (disabled()) {
33
+ event.preventDefault()
34
+ event.stopImmediatePropagation()
35
+ return
36
+ }
37
+
38
+ if (Array.isArray(handler)) {
39
+ const [callback, data] = handler as [(data: unknown, event: MouseEvent) => void, unknown]
40
+ callback(data, event)
41
+ } else if (typeof handler === 'function') {
42
+ const callback = handler as (event: MouseEvent) => void
43
+ callback(event)
44
+ }
45
+ }
46
+ }
47
+
48
+ type ButtonTargetProps<T extends ValidComponent = 'button'> = Omit<ButtonProps<T>, 'as'> & {
49
+ actionAs?: T
50
+ }
51
+
52
+ /** Adapter keeps TooltipTrigger's `as` separate from Button's polymorphic `as`. */
53
+ function ButtonTarget<T extends ValidComponent = 'button'>(props: ButtonTargetProps<T>) {
54
+ const [local, rest] = splitProps(props as ButtonTargetProps, ['actionAs'])
55
+ const PolymorphicButton = Button as (targetProps: ButtonProps<T>) => JSX.Element
56
+ return <PolymorphicButton as={(local.actionAs ?? 'button') as T} {...(rest as ButtonProps<T>)} />
57
+ }
58
+
59
+ export function ActionButton<T extends ValidComponent = 'button'>(props: ActionButtonProps<T>) {
60
+ const [local, rest] = splitProps(props as ActionButtonProps, [
61
+ 'as',
62
+ 'variant',
63
+ 'size',
64
+ 'class',
65
+ 'children',
66
+ 'disabled',
67
+ 'aria-busy',
68
+ 'tooltip',
69
+ 'tooltipSide',
70
+ 'busy',
71
+ 'busyLabel',
72
+ ])
73
+ const busy = () => local.busy ?? false
74
+ const disabled = () => busy() || !!local.disabled
75
+ const busyStatus = () => local.busyLabel ?? 'Working'
76
+ const onClickCapture = (rest as { onClickCapture?: unknown }).onClickCapture
77
+ const PolymorphicButton = Button as (buttonProps: ButtonProps<T>) => JSX.Element
78
+ const PolymorphicButtonTarget = ButtonTarget as (targetProps: ButtonTargetProps<T>) => JSX.Element
79
+
80
+ const buttonProps = () => ({
81
+ ...rest,
82
+ variant: local.variant,
83
+ size: local.size,
84
+ class: [local.class, local.tooltip && disabled() && 'aria-disabled:pointer-events-auto']
85
+ .filter(Boolean)
86
+ .join(' '),
87
+ disabled: disabled() && !local.tooltip,
88
+ 'aria-disabled': disabled() ? true : rest['aria-disabled'],
89
+ tabIndex: local.as === 'a' && disabled() ? (local.tooltip ? 0 : -1) : rest.tabIndex,
90
+ role: local.as === 'a' && disabled() ? 'link' : rest.role,
91
+ href: local.as === 'a' && disabled() ? undefined : (rest as { href?: string }).href,
92
+ 'aria-busy': busy() || local['aria-busy'],
93
+ onClick: guardedClick(rest.onClick, disabled),
94
+ onClickCapture: onClickCapture ? guardedClick(onClickCapture, disabled) : undefined,
95
+ 'on:click': guardedClick((rest as Record<string, unknown>)['on:click'], disabled),
96
+ 'oncapture:click': guardedClick((rest as Record<string, unknown>)['oncapture:click'], disabled),
97
+ })
98
+
99
+ const contents = () => (
100
+ <>
101
+ <Show when={busy()}>
102
+ <Spinner size="sm" label={false} aria-hidden="true" data-icon="inline-start" />
103
+ </Show>
104
+ {local.children}
105
+ </>
106
+ )
107
+
108
+ return (
109
+ <>
110
+ <Show
111
+ when={local.tooltip}
112
+ fallback={
113
+ <PolymorphicButton
114
+ as={(local.as ?? 'button') as T}
115
+ {...(buttonProps() as unknown as ButtonProps<T>)}
116
+ >
117
+ {contents()}
118
+ </PolymorphicButton>
119
+ }
120
+ >
121
+ <Tooltip>
122
+ <TooltipTrigger
123
+ as={PolymorphicButtonTarget}
124
+ {...(buttonProps() as unknown as ComponentProps<typeof PolymorphicButtonTarget>)}
125
+ actionAs={(local.as ?? 'button') as T}
126
+ >
127
+ {contents()}
128
+ </TooltipTrigger>
129
+ <TooltipContent side={local.tooltipSide}>{local.tooltip}</TooltipContent>
130
+ </Tooltip>
131
+ </Show>
132
+ <Show when={busy()}>
133
+ <span role="status" aria-live="polite" class="visually-hidden">
134
+ {busyStatus()}
135
+ </span>
136
+ </Show>
137
+ </>
138
+ )
139
+ }
@@ -0,0 +1,2 @@
1
+ /** A shared Button with optional tooltip and busy-state feedback. */
2
+ export { ActionButton, type ActionButtonProps } from './action-button'