@adea-ai/ui 0.81.1 → 0.82.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.
- package/README.md +41 -8
- package/dist/components/composites/account-menu/account-menu.d.ts +5 -0
- package/dist/components/composites/account-menu/account-menu.d.ts.map +1 -1
- package/dist/components/composites/account-menu/account-menu.js +122 -70
- package/dist/components/composites/account-menu/account-menu.js.map +1 -1
- package/dist/components/composites/action-button/action-button.d.ts +27 -0
- package/dist/components/composites/action-button/action-button.d.ts.map +1 -0
- package/dist/components/composites/action-button/action-button.js +121 -0
- package/dist/components/composites/action-button/action-button.js.map +1 -0
- package/dist/components/composites/action-button/index.d.ts +3 -0
- package/dist/components/composites/action-button/index.d.ts.map +1 -0
- package/dist/components/composites/action-button/index.js +2 -0
- package/dist/components/conversation/thread-panel.d.ts +2 -0
- package/dist/components/conversation/thread-panel.d.ts.map +1 -1
- package/dist/components/conversation/thread-panel.js +36 -31
- package/dist/components/conversation/thread-panel.js.map +1 -1
- package/dist/components/layout/sidebar-nav/index.d.ts +1 -1
- package/dist/components/layout/sidebar-nav/index.d.ts.map +1 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts +6 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts.map +1 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav.js +66 -62
- package/dist/components/layout/sidebar-nav/sidebar-nav.js.map +1 -1
- package/dist/components/ui/board/board.d.ts.map +1 -1
- package/dist/components/ui/board/board.js +54 -7
- package/dist/components/ui/board/board.js.map +1 -1
- package/dist/components/ui/command/command.d.ts +5 -1
- package/dist/components/ui/command/command.d.ts.map +1 -1
- package/dist/components/ui/command/command.js +8 -3
- package/dist/components/ui/command/command.js.map +1 -1
- package/dist/components/ui/input/index.d.ts +1 -1
- package/dist/components/ui/input/index.d.ts.map +1 -1
- package/dist/components/ui/input/input.d.ts +5 -1
- package/dist/components/ui/input/input.d.ts.map +1 -1
- package/dist/components/ui/input/input.js +36 -17
- package/dist/components/ui/input/input.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/lint/index.d.ts +2 -0
- package/dist/lint/index.d.ts.map +1 -1
- package/dist/lint.js +82 -0
- package/dist/lint.js.map +1 -1
- package/dist/r/account-menu.json +1 -0
- package/dist/r/action-button.json +24 -0
- package/dist/r/conversation.json +1 -0
- package/dist/r/registry.json +26 -0
- package/dist/r/src/components/composites/account-menu/account-menu.tsx +100 -44
- package/dist/r/src/components/composites/action-button/action-button.tsx +139 -0
- package/dist/r/src/components/composites/action-button/index.ts +2 -0
- package/dist/r/src/components/conversation/thread-panel.tsx +22 -12
- package/dist/r/src/components/layout/sidebar-nav/index.ts +1 -0
- package/dist/r/src/components/layout/sidebar-nav/sidebar-nav.tsx +29 -21
- package/dist/r/src/components/ui/board/board.tsx +74 -6
- package/dist/r/src/components/ui/command/command.tsx +15 -4
- package/dist/r/src/components/ui/input/index.ts +1 -1
- package/dist/r/src/components/ui/input/input.tsx +38 -21
- package/package.json +1 -1
- package/registry.json +26 -0
- package/src/components/composites/account-menu/account-menu.tsx +100 -44
- package/src/components/composites/action-button/action-button.tsx +139 -0
- package/src/components/composites/action-button/index.ts +2 -0
- package/src/components/conversation/thread-panel.tsx +22 -12
- package/src/components/layout/sidebar-nav/index.ts +1 -0
- package/src/components/layout/sidebar-nav/sidebar-nav.tsx +29 -21
- package/src/components/ui/board/board.tsx +74 -6
- package/src/components/ui/command/command.tsx +15 -4
- package/src/components/ui/input/index.ts +1 -1
- package/src/components/ui/input/input.tsx +38 -21
- package/src/index.ts +1 -0
- package/src/lint/index.ts +95 -0
package/dist/lint.js
CHANGED
|
@@ -26,6 +26,86 @@ var ELEMENT_PRIMITIVES = Object.freeze({
|
|
|
26
26
|
path: "@adea-ai/ui/components/ui/label"
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
|
+
/** Reads a static object-property key without evaluating a computed expression. */
|
|
30
|
+
var staticPropertyKey = (node, computed) => {
|
|
31
|
+
if (!computed && node?.type === "Identifier") return node.name;
|
|
32
|
+
if (node?.type === "Literal" && typeof node.value === "string") return node.value;
|
|
33
|
+
if (node?.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0].value.cooked ?? node.quasis[0].value.raw;
|
|
34
|
+
return null;
|
|
35
|
+
};
|
|
36
|
+
/** Detects named props in a literal JSX spread, including nested literal spreads. */
|
|
37
|
+
var objectHasStaticProperty = (node, name, seen = /* @__PURE__ */ new Set()) => {
|
|
38
|
+
if (!node || seen.has(node)) return false;
|
|
39
|
+
seen.add(node);
|
|
40
|
+
if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression" || node.type === "TSNonNullExpression") return objectHasStaticProperty(node.expression, name, seen);
|
|
41
|
+
if (node.type !== "ObjectExpression") return false;
|
|
42
|
+
return node.properties.some((property) => {
|
|
43
|
+
if (property.type === "Property") return staticPropertyKey(property.key, property.computed === true) === name;
|
|
44
|
+
if (property.type === "SpreadElement") return objectHasStaticProperty(property.argument, name, seen);
|
|
45
|
+
return false;
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
var noInlineStyles = {
|
|
49
|
+
meta: {
|
|
50
|
+
type: "problem",
|
|
51
|
+
docs: { description: "Consumer components cannot set appearance or layout with JSX style props; use shared variants and token-backed classes." },
|
|
52
|
+
schema: [],
|
|
53
|
+
messages: {
|
|
54
|
+
inlineStyle: "JSX style attributes are not permitted, including layout values and CSS custom properties. Use shared variants and token-backed classes.",
|
|
55
|
+
inlineStyleElement: "A JSX style element bypasses the shared design system. Use shared variants and token-backed classes.",
|
|
56
|
+
inlineStyleSpread: "A JSX spread contains a style prop. Pass named shared component props instead of hiding inline styles in a spread."
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
create(context) {
|
|
60
|
+
return {
|
|
61
|
+
JSXAttribute(node) {
|
|
62
|
+
if (node.name?.type === "JSXIdentifier" && node.name.name === "style") context.report({
|
|
63
|
+
node,
|
|
64
|
+
messageId: "inlineStyle"
|
|
65
|
+
});
|
|
66
|
+
},
|
|
67
|
+
JSXOpeningElement(node) {
|
|
68
|
+
if (node.name?.type === "JSXIdentifier" && node.name.name === "style") context.report({
|
|
69
|
+
node,
|
|
70
|
+
messageId: "inlineStyleElement"
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
JSXSpreadAttribute(node) {
|
|
74
|
+
if (objectHasStaticProperty(node.argument, "style")) context.report({
|
|
75
|
+
node,
|
|
76
|
+
messageId: "inlineStyleSpread"
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var noClassList = {
|
|
83
|
+
meta: {
|
|
84
|
+
type: "problem",
|
|
85
|
+
docs: { description: "Solid classList props hide conditional classes from static design-system lint; use cn() with its object-key form." },
|
|
86
|
+
schema: [],
|
|
87
|
+
messages: {
|
|
88
|
+
classList: "classList hides classes from design-system lint. Use cn() with its object-key form and statically readable class names.",
|
|
89
|
+
classListSpread: "A JSX spread contains classList. Use cn() with its object-key form instead of hiding classes in a spread."
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
create(context) {
|
|
93
|
+
return {
|
|
94
|
+
JSXAttribute(node) {
|
|
95
|
+
if (node.name?.type === "JSXIdentifier" && node.name.name === "classList") context.report({
|
|
96
|
+
node,
|
|
97
|
+
messageId: "classList"
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
JSXSpreadAttribute(node) {
|
|
101
|
+
if (objectHasStaticProperty(node.argument, "classList")) context.report({
|
|
102
|
+
node,
|
|
103
|
+
messageId: "classListSpread"
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
};
|
|
29
109
|
var noRawInteractiveElements = {
|
|
30
110
|
meta: {
|
|
31
111
|
type: "problem",
|
|
@@ -188,6 +268,8 @@ var hasTabStop = (node) => (node.attributes ?? []).some((attribute) => {
|
|
|
188
268
|
var plugin = {
|
|
189
269
|
meta: { name: "adea" },
|
|
190
270
|
rules: {
|
|
271
|
+
"no-inline-styles": noInlineStyles,
|
|
272
|
+
"no-class-list": noClassList,
|
|
191
273
|
"no-raw-interactive-elements": noRawInteractiveElements,
|
|
192
274
|
"no-primitive-library-imports": {
|
|
193
275
|
meta: {
|
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 '@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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"}
|
|
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\n/** Reads a static object-property key without evaluating a computed expression. */\nconst staticPropertyKey = (node: any, computed: boolean): string | null => {\n if (!computed && node?.type === 'Identifier') return node.name\n if (node?.type === 'Literal' && typeof node.value === 'string') return node.value\n if (node?.type === 'TemplateLiteral' && node.expressions.length === 0)\n return node.quasis[0].value.cooked ?? node.quasis[0].value.raw\n return null\n}\n\n/** Detects named props in a literal JSX spread, including nested literal spreads. */\nconst objectHasStaticProperty = (node: any, name: string, seen = new Set<unknown>()): boolean => {\n if (!node || seen.has(node)) return false\n seen.add(node)\n if (\n node.type === 'TSAsExpression' ||\n node.type === 'TSSatisfiesExpression' ||\n node.type === 'TSNonNullExpression'\n )\n return objectHasStaticProperty(node.expression, name, seen)\n if (node.type !== 'ObjectExpression') return false\n return node.properties.some((property: any) => {\n if (property.type === 'Property')\n return staticPropertyKey(property.key, property.computed === true) === name\n if (property.type === 'SpreadElement')\n return objectHasStaticProperty(property.argument, name, seen)\n return false\n })\n}\n\nconst noInlineStyles: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Consumer components cannot set appearance or layout with JSX style props; use shared variants and token-backed classes.',\n },\n schema: [],\n messages: {\n inlineStyle:\n 'JSX style attributes are not permitted, including layout values and CSS custom properties. Use shared variants and token-backed classes.',\n inlineStyleElement:\n 'A JSX style element bypasses the shared design system. Use shared variants and token-backed classes.',\n inlineStyleSpread:\n 'A JSX spread contains a style prop. Pass named shared component props instead of hiding inline styles in a spread.',\n },\n },\n create(context) {\n return {\n JSXAttribute(node: any) {\n if (node.name?.type === 'JSXIdentifier' && node.name.name === 'style')\n context.report({ node, messageId: 'inlineStyle' })\n },\n JSXOpeningElement(node: any) {\n if (node.name?.type === 'JSXIdentifier' && node.name.name === 'style')\n context.report({ node, messageId: 'inlineStyleElement' })\n },\n JSXSpreadAttribute(node: any) {\n if (objectHasStaticProperty(node.argument, 'style'))\n context.report({ node, messageId: 'inlineStyleSpread' })\n },\n }\n },\n}\n\nconst noClassList: RuleModule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Solid classList props hide conditional classes from static design-system lint; use cn() with its object-key form.',\n },\n schema: [],\n messages: {\n classList:\n 'classList hides classes from design-system lint. Use cn() with its object-key form and statically readable class names.',\n classListSpread:\n 'A JSX spread contains classList. Use cn() with its object-key form instead of hiding classes in a spread.',\n },\n },\n create(context) {\n return {\n JSXAttribute(node: any) {\n if (node.name?.type === 'JSXIdentifier' && node.name.name === 'classList')\n context.report({ node, messageId: 'classList' })\n },\n JSXSpreadAttribute(node: any) {\n if (objectHasStaticProperty(node.argument, 'classList'))\n context.report({ node, messageId: 'classListSpread' })\n },\n }\n },\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-inline-styles': noInlineStyles,\n 'no-class-list': noClassList,\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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"}
|
package/dist/r/account-menu.json
CHANGED
|
@@ -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
|
+
}
|
package/dist/r/conversation.json
CHANGED
package/dist/r/registry.json
CHANGED
|
@@ -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
|
-
<
|
|
99
|
-
<
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
}
|