@adea-ai/ui 0.138.0 → 0.139.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.
@@ -1 +1 @@
1
- {"version":3,"file":"callout.js","names":["ComponentProps","splitProps","cva","VariantProps","cn","calloutVariants","variants","variant","default","destructive","success","warning","info","size","sm","md","lg","defaultVariants","CalloutProps","Callout","props","local","rest","_el$","_tmpl$","_$spread","_$mergeProps","class","CalloutTitle","_el$2","_tmpl$2","_$mergeProps"],"sources":["../../../../src/components/ui/callout/callout.tsx"],"sourcesContent":["import type { ComponentProps } from 'solid-js'\nimport { splitProps } from 'solid-js'\nimport { cva, type VariantProps } from '#lib/variants'\nimport { cn } from '#lib/utils'\n\n/**\n * Callout.\n *\n * A quiet aside that belongs to the content around it: the summary at the top of\n * an answer, a note in documentation, a remembered fact beside the reply that\n * used it. It is a card-coloured block with one rule on its start edge, and\n * nothing about it interrupts the reader.\n *\n * That is the difference from `Alert`. An `Alert` is a message about the state of\n * the surface it sits in — a failed sync, a validation summary — so it is always\n * `role=\"alert\"`, a live announcement, with a tone glyph and a tinted status\n * surface. A callout is static page content: a plain `div` with `role=\"note\"`, no\n * live region, no icon, and no tint. A caller whose note really is a status\n * message uses `Alert`; one whose note needs a different landmark role passes\n * `role`. Reach for `Card` when the block is a self-contained object with its own\n * header, content and footer; a callout has no parts beyond an optional\n * `CalloutTitle`.\n *\n * The rule is the only thing that varies by `variant`: `default` is the accent\n * (`--primary`), and the four status hues use the same tokens `Alert` does. The\n * rule is a logical `border-s-*`, so it moves to the right in RTL. The corners\n * stay square because a rounded box would curve the rule into a crescent.\n *\n * `size` is the padding rung and the rule's weight together, so a caller never\n * sets `p-*` or a border width to make one denser. The rungs are Cortana's three\n * answer-view callouts, expressed in the Tailwind spacing scale:\n *\n * - `lg`: 22px / 24px padding, 3px rule — Cortana's `.answer-copy`.\n * - `md`: 18px / 20px padding, 3px rule — Cortana's `.answer-memory`.\n * - `sm`: 12px / 14px padding, 2px rule — Cortana's `.answer-note`.\n *\n * Type size and colour are inherited rather than set (only the card foreground is\n * applied), so a callout in a dense panel and one in an article both read at the\n * size of the text around them.\n *\n * Text that has no break opportunity (a 200-character token, a URL, a file path)\n * wraps with `overflow-wrap: anywhere` rather than overflowing the box. `anywhere`\n * and not `break-word`, because only `anywhere` lowers the box's min-content width:\n * a callout that is a flex or grid item (auto minimum size) then shrinks to its\n * track instead of stretching it. The callout's own padding, rule and width are\n * untouched, so none of its sizing differs from Cortana's.\n *\n * The block is deliberately not a flex or grid container: its children are inline\n * text, links and code, and a flex root would turn each of those into a separate\n * item and break the line.\n */\nexport const calloutVariants = cva(\n 'bg-card text-card-foreground block rounded-none wrap-anywhere',\n {\n variants: {\n variant: {\n default: 'border-s-primary',\n destructive: 'border-s-destructive',\n success: 'border-s-success',\n warning: 'border-s-warning',\n info: 'border-s-info',\n },\n size: {\n /** 12px block / 14px inline padding, 2px rule: a short inline note. */\n sm: 'border-s-2 px-3.5 py-3',\n /** 18px block / 20px inline padding, 3px rule: a supporting aside. */\n md: 'border-s-3 px-5 py-4.5',\n /** 22px block / 24px inline padding, 3px rule: the lead summary of a view. */\n lg: 'border-s-3 px-6 py-5.5',\n },\n },\n defaultVariants: { variant: 'default', size: 'md' },\n }\n)\n\nexport type CalloutProps = ComponentProps<'div'> & VariantProps<typeof calloutVariants>\n\nexport function Callout(props: CalloutProps) {\n const [local, rest] = splitProps(props, ['class', 'variant', 'size'])\n return (\n <div\n data-slot=\"callout\"\n role=\"note\"\n class={cn(calloutVariants({ variant: local.variant, size: local.size }), local.class)}\n {...rest}\n />\n )\n}\n\n/**\n * An optional heading line for the callout. It is a styled `div`, not a heading\n * element: a callout sits inside someone else's outline, and a fixed heading\n * level would break it. Pass `role=\"heading\"` with `aria-level` where the\n * content does belong in the outline.\n *\n * The title always carries a 4px (`mb-1`) margin below it, whatever follows: a\n * paragraph, a list, or bare text. CSS cannot see text nodes, so a selector such\n * as `:last-child` or `:only-child` cannot tell `<Title/>text` (needs the gap)\n * from `<Title/>` (needs none), and `not-last:mb-1` silently dropped the gap for\n * bare text, the most common body. The unconditional margin is the less\n * surprising failure: it is never missing where text follows. The price is the\n * title-only callout, which would end 4px taller than its padding says. Pass\n * `class=\"mb-0\"` there (`cn` lets the caller's margin win); the `WithTitle` story\n * shows it.\n */\nexport function CalloutTitle(props: ComponentProps<'div'>) {\n const [local, rest] = splitProps(props, ['class'])\n return (\n <div\n data-slot=\"callout-title\"\n class={cn('mb-1 font-medium tracking-tight', local.class)}\n {...rest}\n />\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,IAAaK,kBAAkBH,IAC7B,iEACA;CACEI,UAAU;EACRC,SAAS;GACPC,SAAS;GACTC,aAAa;GACbC,SAAS;GACTC,SAAS;GACTC,MAAM;EACR;EACAC,MAAM;;GAEJC,IAAI;;GAEJC,IAAI;;GAEJC,IAAI;EACN;CACF;CACAC,iBAAiB;EAAEV,SAAS;EAAWM,MAAM;CAAK;AACpD,CACF;AAIA,SAAgBM,QAAQC,OAAqB;CAC3C,MAAM,CAACC,OAAOC,QAAQrB,WAAWmB,OAAO;EAAC;EAAS;EAAW;CAAM,CAAC;CACpE,cAAA;EAAA,IAAAG,OAAAC,OAAA;EAAAC,OAAAF,MAAAG,WAAA,EAAA,KAAA,WAAA;GAAA,OAIWtB,GAAGC,gBAAgB;IAAEE,SAASc,MAAMd;IAASM,MAAMQ,MAAMR;GAAK,CAAC,GAAGQ,MAAMM,KAAK;EAAC,EAAA,GACjFL,IAAI,GAAA,OAAA,KAAA;EAAA,OAAAC;CAAA,EAAA,CAAA;AAGd;;;;;;;;;;;;;;;;;AAkBA,SAAgBK,aAAaR,OAA8B;CACzD,MAAM,CAACC,OAAOC,QAAQrB,WAAWmB,OAAO,CAAC,OAAO,CAAC;CACjD,cAAA;EAAA,IAAAS,QAAAC,QAAA;EAAAL,OAAAI,OAAAH,WAAA,EAAA,KAAA,WAAA;GAAA,OAGWtB,GAAG,mCAAmCiB,MAAMM,KAAK;EAAC,EAAA,GACrDL,IAAI,GAAA,OAAA,KAAA;EAAA,OAAAO;CAAA,EAAA,CAAA;AAGd"}
1
+ {"version":3,"file":"callout.js","names":["ComponentProps","children","createMemo","splitProps","cva","VariantProps","cn","calloutVariants","variants","variant","default","destructive","success","warning","info","size","sm","md","lg","defaultVariants","CalloutProps","isRendered","node","trim","isCalloutTitle","getAttribute","Callout","props","local","rest","resolved","titleOnly","nodes","toArray","filter","length","_el$","_tmpl$","_$spread","_$mergeProps","data-title-only","undefined","class","_$insert","CalloutTitle","_el$2","_tmpl$2","_$mergeProps"],"sources":["../../../../src/components/ui/callout/callout.tsx"],"sourcesContent":["import type { ComponentProps } from 'solid-js'\nimport { children, createMemo, splitProps } from 'solid-js'\nimport { cva, type VariantProps } from '#lib/variants'\nimport { cn } from '#lib/utils'\n\n/**\n * Callout.\n *\n * A quiet aside that belongs to the content around it: the summary at the top of\n * an answer, a note in documentation, a remembered fact beside the reply that\n * used it. It is a card-coloured block with one rule on its start edge, and\n * nothing about it interrupts the reader.\n *\n * That is the difference from `Alert`. An `Alert` is a message about the state of\n * the surface it sits in — a failed sync, a validation summary — so it is always\n * `role=\"alert\"`, a live announcement, with a tone glyph and a tinted status\n * surface. A callout is static page content: a plain `div` with `role=\"note\"`, no\n * live region, no icon, and no tint. A caller whose note really is a status\n * message uses `Alert`; one whose note needs a different landmark role passes\n * `role`. Reach for `Card` when the block is a self-contained object with its own\n * header, content and footer; a callout has no parts beyond an optional\n * `CalloutTitle`.\n *\n * The rule is the only thing that varies by `variant`: `default` is the accent\n * (`--primary`), and the four status hues use the same tokens `Alert` does. The\n * rule is a logical `border-s-*`, so it moves to the right in RTL. The corners\n * stay square because a rounded box would curve the rule into a crescent.\n *\n * `size` is the padding rung and the rule's weight together, so a caller never\n * sets `p-*` or a border width to make one denser. The rungs are Cortana's three\n * answer-view callouts, expressed in the Tailwind spacing scale:\n *\n * - `lg`: 22px / 24px padding, 3px rule — Cortana's `.answer-copy`.\n * - `md`: 18px / 20px padding, 3px rule — Cortana's `.answer-memory`.\n * - `sm`: 12px / 14px padding, 2px rule — Cortana's `.answer-note`.\n *\n * Type size and colour are inherited rather than set (only the card foreground is\n * applied), so a callout in a dense panel and one in an article both read at the\n * size of the text around them.\n *\n * Text that has no break opportunity (a 200-character token, a URL, a file path)\n * wraps with `overflow-wrap: anywhere` rather than overflowing the box. `anywhere`\n * and not `break-word`, because only `anywhere` lowers the box's min-content width:\n * a callout that is a flex or grid item (auto minimum size) then shrinks to its\n * track instead of stretching it. The callout's own padding, rule and width are\n * untouched, so none of its sizing differs from Cortana's.\n *\n * The block is deliberately not a flex or grid container: its children are inline\n * text, links and code, and a flex root would turn each of those into a separate\n * item and break the line.\n */\nexport const calloutVariants = cva(\n 'bg-card text-card-foreground block rounded-none wrap-anywhere',\n {\n variants: {\n variant: {\n default: 'border-s-primary',\n destructive: 'border-s-destructive',\n success: 'border-s-success',\n warning: 'border-s-warning',\n info: 'border-s-info',\n },\n size: {\n /** 12px block / 14px inline padding, 2px rule: a short inline note. */\n sm: 'border-s-2 px-3.5 py-3',\n /** 18px block / 20px inline padding, 3px rule: a supporting aside. */\n md: 'border-s-3 px-5 py-4.5',\n /** 22px block / 24px inline padding, 3px rule: the lead summary of a view. */\n lg: 'border-s-3 px-6 py-5.5',\n },\n },\n defaultVariants: { variant: 'default', size: 'md' },\n }\n)\n\nexport type CalloutProps = ComponentProps<'div'> & VariantProps<typeof calloutVariants>\n\nconst isRendered = (node: unknown) =>\n node != null && typeof node !== 'boolean' && (typeof node !== 'string' || node.trim() !== '')\n\nconst isCalloutTitle = (node: unknown) =>\n typeof node === 'object' &&\n node !== null &&\n 'getAttribute' in node &&\n typeof node.getAttribute === 'function' &&\n node.getAttribute('data-slot') === 'callout-title'\n\nexport function Callout(props: CalloutProps) {\n const [local, rest] = splitProps(props, ['class', 'variant', 'size', 'children'])\n const resolved = children(() => local.children)\n // CSS cannot see text nodes, so the callout looks at what it renders and marks a\n // title with nothing after it. The title reads the mark to drop its bottom margin.\n const titleOnly = createMemo(() => {\n const nodes = resolved.toArray().filter(isRendered)\n return nodes.length === 1 && isCalloutTitle(nodes[0])\n })\n return (\n <div\n data-slot=\"callout\"\n data-title-only={titleOnly() ? '' : undefined}\n role=\"note\"\n class={cn(calloutVariants({ variant: local.variant, size: local.size }), local.class)}\n {...rest}\n >\n {resolved()}\n </div>\n )\n}\n\n/**\n * An optional heading line for the callout. It is a styled `div`, not a heading\n * element: a callout sits inside someone else's outline, and a fixed heading\n * level would break it. Pass `role=\"heading\"` with `aria-level` where the\n * content does belong in the outline.\n *\n * The title carries a 4px (`mb-1`) margin below it, whatever follows: a\n * paragraph, a list, or bare text. CSS cannot see text nodes, so a selector such\n * as `:last-child` or `:only-child` cannot tell `<Title/>text` (needs the gap)\n * from `<Title/>` (needs none), and `not-last:mb-1` silently dropped the gap for\n * bare text, the most common body. So the gap stays the default and the one case\n * that does not want it is detected by `Callout`, which sees its rendered\n * children: a callout whose only child is a title carries `data-title-only`, and\n * the title drops its margin so the block ends at its own padding. A caller never\n * passes `mb-0` for this. Anything else after the title, text included, keeps the\n * gap.\n */\nexport function CalloutTitle(props: ComponentProps<'div'>) {\n const [local, rest] = splitProps(props, ['class'])\n return (\n <div\n data-slot=\"callout-title\"\n class={cn('mb-1 font-medium tracking-tight in-data-title-only:mb-0', local.class)}\n {...rest}\n />\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,IAAaO,kBAAkBH,IAC7B,iEACA;CACEI,UAAU;EACRC,SAAS;GACPC,SAAS;GACTC,aAAa;GACbC,SAAS;GACTC,SAAS;GACTC,MAAM;EACR;EACAC,MAAM;;GAEJC,IAAI;;GAEJC,IAAI;;GAEJC,IAAI;EACN;CACF;CACAC,iBAAiB;EAAEV,SAAS;EAAWM,MAAM;CAAK;AACpD,CACF;AAIA,IAAMM,cAAcC,SAClBA,QAAQ,QAAQ,OAAOA,SAAS,cAAc,OAAOA,SAAS,YAAYA,KAAKC,KAAK,MAAM;AAE5F,IAAMC,kBAAkBF,SACtB,OAAOA,SAAS,YAChBA,SAAS,QACT,kBAAkBA,QAClB,OAAOA,KAAKG,iBAAiB,cAC7BH,KAAKG,aAAa,WAAW,MAAM;AAErC,SAAgBC,QAAQC,OAAqB;CAC3C,MAAM,CAACC,OAAOC,QAAQ1B,WAAWwB,OAAO;EAAC;EAAS;EAAW;EAAQ;CAAU,CAAC;CAChF,MAAMG,WAAW7B,eAAe2B,MAAM3B,QAAQ;CAG9C,MAAM8B,YAAY7B,iBAAiB;EACjC,MAAM8B,QAAQF,SAASG,QAAQ,CAAC,CAACC,OAAOb,UAAU;EAClD,OAAOW,MAAMG,WAAW,KAAKX,eAAeQ,MAAM,EAAE;CACtD,CAAC;CACD,cAAA;EAAA,IAAAI,OAAAC,OAAA;EAAAC,OAAAF,MAAAG,WAAA;GAAA,KAAA,qBAAA;IAAA,OAGqBR,UAAU,IAAI,KAAKU,KAAAA;GAAS;GAAA,KAAA,WAAA;IAAA,OAEtCnC,GAAGC,gBAAgB;KAAEE,SAASmB,MAAMnB;KAASM,MAAMa,MAAMb;IAAK,CAAC,GAAGa,MAAMc,KAAK;GAAC;EAAA,GACjFb,IAAI,GAAA,OAAA,IAAA;EAAAc,OAAAP,MAEPN,QAAQ;EAAA,OAAAM;CAAA,EAAA,CAAA;AAGf;;;;;;;;;;;;;;;;;;AAmBA,SAAgBQ,aAAajB,OAA8B;CACzD,MAAM,CAACC,OAAOC,QAAQ1B,WAAWwB,OAAO,CAAC,OAAO,CAAC;CACjD,cAAA;EAAA,IAAAkB,QAAAC,QAAA;EAAAR,OAAAO,OAAAN,WAAA,EAAA,KAAA,WAAA;GAAA,OAGWjC,GAAG,2DAA2DsB,MAAMc,KAAK;EAAC,EAAA,GAC7Eb,IAAI,GAAA,OAAA,KAAA;EAAA,OAAAgB;CAAA,EAAA,CAAA;AAGd"}
@@ -1 +1 @@
1
- {"version":3,"file":"secret-input-group.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/input-group/secret-input-group.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,GAAG,EAAE,MAAM,UAAU,CAAA;AAKhF,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,GAAG,CAAC,YAAY,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;IACnD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,IAAI,GAAG,YAAY,GAAG,kBAAkB,GAAG,cAAc,CAAC,CAAA;AAE5F;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,eAiD5D"}
1
+ {"version":3,"file":"secret-input-group.d.ts","sourceRoot":"","sources":["../../../../src/components/ui/input-group/secret-input-group.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAgC,KAAK,cAAc,EAAE,KAAK,GAAG,EAAE,MAAM,UAAU,CAAA;AAKtF,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,OAAO,CAAA;IACjB,QAAQ,EAAE,GAAG,CAAC,YAAY,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAA;IACnD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,IAAI,GAAG,YAAY,GAAG,kBAAkB,GAAG,cAAc,CAAC,CAAA;AAE5F;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,qBAAqB,eAoD5D"}
@@ -2,7 +2,7 @@ import { FormFieldContext } from "../../../lib/form-field.js";
2
2
  import { ActionButton } from "../../composites/action-button/action-button.js";
3
3
  import { InputGroup, InputGroupInput } from "./input-group.js";
4
4
  import { createComponent, memo, mergeProps } from "solid-js/web";
5
- import { splitProps, useContext } from "solid-js";
5
+ import { Show, splitProps, useContext } from "solid-js";
6
6
  //#region src/components/ui/input-group/secret-input-group.tsx
7
7
  /**
8
8
  * A password input with an optional keyboard-accessible clear action.
@@ -40,34 +40,35 @@ function SecretInputGroup(props) {
40
40
  return local.onChange;
41
41
  }
42
42
  }));
43
- if (!local.onClear) return createComponent(InputGroup, {
44
- get ["class"]() {
45
- return local.class;
46
- },
47
- children: input
48
- });
49
43
  return createComponent(InputGroup, {
50
44
  get ["class"]() {
51
45
  return local.class;
52
46
  },
53
47
  get children() {
54
- return [input, createComponent(ActionButton, {
55
- "data-slot": "input-group-button",
56
- variant: "ghost",
57
- size: "xs",
58
- "class": "-me-1 h-6 min-w-0 shrink-0 px-2",
59
- get disabled() {
60
- return local.disabled;
61
- },
62
- get tooltip() {
63
- return memo(() => !!local.disabled)() ? local.disabledReason ?? "This API key is managed by policy" : local.clearLabel ?? "Clear API key";
64
- },
65
- tooltipSide: "top",
66
- get ["aria-label"]() {
67
- return local.clearLabel ?? "Clear API key";
48
+ return [input, createComponent(Show, {
49
+ get when() {
50
+ return local.onClear;
68
51
  },
69
- onClick: () => local.onClear?.(),
70
- children: "Clear"
52
+ get children() {
53
+ return createComponent(ActionButton, {
54
+ "data-slot": "input-group-button",
55
+ variant: "ghost",
56
+ size: "xs",
57
+ "class": "-me-1 h-6 min-w-0 shrink-0 px-2",
58
+ get disabled() {
59
+ return local.disabled;
60
+ },
61
+ get tooltip() {
62
+ return memo(() => !!local.disabled)() ? local.disabledReason ?? "This API key is managed by policy" : local.clearLabel ?? "Clear API key";
63
+ },
64
+ tooltipSide: "top",
65
+ get ["aria-label"]() {
66
+ return local.clearLabel ?? "Clear API key";
67
+ },
68
+ onClick: () => local.onClear?.(),
69
+ children: "Clear"
70
+ });
71
+ }
71
72
  })];
72
73
  }
73
74
  });
@@ -1 +1 @@
1
- {"version":3,"file":"secret-input-group.js","names":["splitProps","useContext","ComponentProps","JSX","ActionButton","FormFieldContext","InputGroup","InputGroupInput","SecretInputGroupProps","value","disabled","onChange","EventHandler","HTMLInputElement","Event","onClear","clearLabel","disabledReason","class","Pick","SecretInputGroup","props","field","local","rest","input","_$createComponent","_$mergeProps","aria-label","undefined","type","autocomplete","onInput","children","variant","size","tooltip","_$memo","tooltipSide","onClick","_$createComponent","_$mergeProps","_$memo"],"sources":["../../../../src/components/ui/input-group/secret-input-group.tsx"],"sourcesContent":["import { splitProps, useContext, type ComponentProps, type JSX } from 'solid-js'\nimport { ActionButton } from '../../composites/action-button'\nimport { FormFieldContext } from '#lib/form-field'\nimport { InputGroup, InputGroupInput } from './input-group'\n\nexport type SecretInputGroupProps = {\n value: string\n disabled: boolean\n onChange: JSX.EventHandler<HTMLInputElement, Event>\n onClear?: () => void\n /**\n * What the clear action does, for this field: its accessible name and its\n * tooltip while enabled (\"Clear stored token\", \"Clear Google token path\").\n * Defaults to \"Clear API key\".\n */\n clearLabel?: string\n /**\n * Why the clear action is unavailable, shown as its tooltip while the group is\n * `disabled`. Defaults to the policy message, so name the real reason when a\n * field is disabled for another one (\"Set a token variable first\").\n */\n disabledReason?: string\n class?: string\n} & Pick<ComponentProps<'input'>, 'id' | 'aria-label' | 'aria-describedby' | 'aria-invalid'>\n\n/**\n * A password input with an optional keyboard-accessible clear action.\n *\n * The wording is the caller's: inside a `FormField` the input takes the field's\n * label, outside one it needs `aria-label` (the fallback names an API key), and\n * `clearLabel` and `disabledReason` name the clear action for the field it\n * belongs to, so a bearer token or a token path is not announced as an API key.\n */\nexport function SecretInputGroup(props: SecretInputGroupProps) {\n const field = useContext(FormFieldContext)\n const [local, rest] = splitProps(props, [\n 'value',\n 'disabled',\n 'onChange',\n 'onClear',\n 'clearLabel',\n 'disabledReason',\n 'aria-label',\n 'class',\n ])\n\n const input = (\n <InputGroupInput\n {...rest}\n aria-label={local['aria-label'] ?? (field ? undefined : 'New API key')}\n type=\"password\"\n autocomplete=\"new-password\"\n value={local.value}\n disabled={local.disabled}\n onInput={local.onChange}\n />\n )\n\n if (!local.onClear) return <InputGroup class={local.class}>{input}</InputGroup>\n\n return (\n <InputGroup class={local.class}>\n {input}\n <ActionButton\n data-slot=\"input-group-button\"\n variant=\"ghost\"\n size=\"xs\"\n class=\"-me-1 h-6 min-w-0 shrink-0 px-2\"\n disabled={local.disabled}\n tooltip={\n local.disabled\n ? (local.disabledReason ?? 'This API key is managed by policy')\n : (local.clearLabel ?? 'Clear API key')\n }\n tooltipSide=\"top\"\n aria-label={local.clearLabel ?? 'Clear API key'}\n onClick={() => local.onClear?.()}\n >\n Clear\n </ActionButton>\n </InputGroup>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;AAiCA,SAAgBoB,iBAAiBC,OAA8B;CAC7D,MAAMC,QAAQrB,WAAWI,gBAAgB;CACzC,MAAM,CAACkB,OAAOC,QAAQxB,WAAWqB,OAAO;EACtC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CAAO,CACR;CAED,MAAMI,QAAKC,gBACRnB,iBAAeoB,WACVH,MAAI;EAAA,KAAA,gBAAA;GAAA,OACID,MAAM,kBAAkBD,QAAQO,KAAAA,IAAY;EAAc;EACtEC,MAAI;EACJC,cAAY;EAAA,IACZtB,QAAK;GAAA,OAAEc,MAAMd;EAAK;EAAA,IAClBC,WAAQ;GAAA,OAAEa,MAAMb;EAAQ;EAAA,IACxBsB,UAAO;GAAA,OAAET,MAAMZ;EAAQ;CAAA,CAAA,CAAA;CAI3B,IAAI,CAACY,MAAMR,SAAS,OAAAW,gBAAQpB,YAAU;EAAA,KAAA,WAAA;GAAA,OAAQiB,MAAML;EAAK;EAAAe,UAAGR;CAAK,CAAA;CAEjE,OAAAC,gBACGpB,YAAU;EAAA,KAAA,WAAA;GAAA,OAAQiB,MAAML;EAAK;EAAA,IAAAe,WAAA;GAAA,OAAA,CAC3BR,OAAKC,gBACLtB,cAAY;IAAA,aAAA;IAEX8B,SAAO;IACPC,MAAI;IAAA,SAAA;IAAA,IAEJzB,WAAQ;KAAA,OAAEa,MAAMb;IAAQ;IAAA,IACxB0B,UAAO;KAAA,OACLC,WAAA,CAAA,CAAAd,MAAMb,QAAQ,CAAA,CAAA,IACTa,MAAMN,kBAAkB,sCACxBM,MAAMP,cAAc;IAAgB;IAE3CsB,aAAW;IAAA,KAAA,gBAAA;KAAA,OACCf,MAAMP,cAAc;IAAe;IAC/CuB,eAAehB,MAAMR,UAAU;IAACkB,UAAA;GAAA,CAAA,CAAA;EAAA;CAAA,CAAA;AAMxC"}
1
+ {"version":3,"file":"secret-input-group.js","names":["Show","splitProps","useContext","ComponentProps","JSX","ActionButton","FormFieldContext","InputGroup","InputGroupInput","SecretInputGroupProps","value","disabled","onChange","EventHandler","HTMLInputElement","Event","onClear","clearLabel","disabledReason","class","Pick","SecretInputGroup","props","field","local","rest","input","_$createComponent","_$mergeProps","aria-label","undefined","type","autocomplete","onInput","children","when","variant","size","tooltip","_$memo","tooltipSide","onClick","_$createComponent","_$mergeProps","_$memo"],"sources":["../../../../src/components/ui/input-group/secret-input-group.tsx"],"sourcesContent":["import { Show, splitProps, useContext, type ComponentProps, type JSX } from 'solid-js'\nimport { ActionButton } from '../../composites/action-button'\nimport { FormFieldContext } from '#lib/form-field'\nimport { InputGroup, InputGroupInput } from './input-group'\n\nexport type SecretInputGroupProps = {\n value: string\n disabled: boolean\n onChange: JSX.EventHandler<HTMLInputElement, Event>\n onClear?: () => void\n /**\n * What the clear action does, for this field: its accessible name and its\n * tooltip while enabled (\"Clear stored token\", \"Clear Google token path\").\n * Defaults to \"Clear API key\".\n */\n clearLabel?: string\n /**\n * Why the clear action is unavailable, shown as its tooltip while the group is\n * `disabled`. Defaults to the policy message, so name the real reason when a\n * field is disabled for another one (\"Set a token variable first\").\n */\n disabledReason?: string\n class?: string\n} & Pick<ComponentProps<'input'>, 'id' | 'aria-label' | 'aria-describedby' | 'aria-invalid'>\n\n/**\n * A password input with an optional keyboard-accessible clear action.\n *\n * The wording is the caller's: inside a `FormField` the input takes the field's\n * label, outside one it needs `aria-label` (the fallback names an API key), and\n * `clearLabel` and `disabledReason` name the clear action for the field it\n * belongs to, so a bearer token or a token path is not announced as an API key.\n */\nexport function SecretInputGroup(props: SecretInputGroupProps) {\n const field = useContext(FormFieldContext)\n const [local, rest] = splitProps(props, [\n 'value',\n 'disabled',\n 'onChange',\n 'onClear',\n 'clearLabel',\n 'disabledReason',\n 'aria-label',\n 'class',\n ])\n\n const input = (\n <InputGroupInput\n {...rest}\n aria-label={local['aria-label'] ?? (field ? undefined : 'New API key')}\n type=\"password\"\n autocomplete=\"new-password\"\n value={local.value}\n disabled={local.disabled}\n onInput={local.onChange}\n />\n )\n\n // One group for both cases: `onClear` may appear or disappear after setup\n // (a stored secret is cleared, or a configured one arrives), and the input\n // must keep its element, value and focus while the action comes and goes.\n return (\n <InputGroup class={local.class}>\n {input}\n <Show when={local.onClear}>\n <ActionButton\n data-slot=\"input-group-button\"\n variant=\"ghost\"\n size=\"xs\"\n class=\"-me-1 h-6 min-w-0 shrink-0 px-2\"\n disabled={local.disabled}\n tooltip={\n local.disabled\n ? (local.disabledReason ?? 'This API key is managed by policy')\n : (local.clearLabel ?? 'Clear API key')\n }\n tooltipSide=\"top\"\n aria-label={local.clearLabel ?? 'Clear API key'}\n onClick={() => local.onClear?.()}\n >\n Clear\n </ActionButton>\n </Show>\n </InputGroup>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;AAiCA,SAAgBqB,iBAAiBC,OAA8B;CAC7D,MAAMC,QAAQrB,WAAWI,gBAAgB;CACzC,MAAM,CAACkB,OAAOC,QAAQxB,WAAWqB,OAAO;EACtC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CAAO,CACR;CAED,MAAMI,QAAKC,gBACRnB,iBAAeoB,WACVH,MAAI;EAAA,KAAA,gBAAA;GAAA,OACID,MAAM,kBAAkBD,QAAQO,KAAAA,IAAY;EAAc;EACtEC,MAAI;EACJC,cAAY;EAAA,IACZtB,QAAK;GAAA,OAAEc,MAAMd;EAAK;EAAA,IAClBC,WAAQ;GAAA,OAAEa,MAAMb;EAAQ;EAAA,IACxBsB,UAAO;GAAA,OAAET,MAAMZ;EAAQ;CAAA,CAAA,CAAA;CAO3B,OAAAe,gBACGpB,YAAU;EAAA,KAAA,WAAA;GAAA,OAAQiB,MAAML;EAAK;EAAA,IAAAe,WAAA;GAAA,OAAA,CAC3BR,OAAKC,gBACL3B,MAAI;IAAA,IAACmC,OAAI;KAAA,OAAEX,MAAMR;IAAO;IAAA,IAAAkB,WAAA;KAAA,OAAAP,gBACtBtB,cAAY;MAAA,aAAA;MAEX+B,SAAO;MACPC,MAAI;MAAA,SAAA;MAAA,IAEJ1B,WAAQ;OAAA,OAAEa,MAAMb;MAAQ;MAAA,IACxB2B,UAAO;OAAA,OACLC,WAAA,CAAA,CAAAf,MAAMb,QAAQ,CAAA,CAAA,IACTa,MAAMN,kBAAkB,sCACxBM,MAAMP,cAAc;MAAgB;MAE3CuB,aAAW;MAAA,KAAA,gBAAA;OAAA,OACChB,MAAMP,cAAc;MAAe;MAC/CwB,eAAejB,MAAMR,UAAU;MAACkB,UAAA;KAAA,CAAA;IAAA;GAAA,CAAA,CAAA;EAAA;CAAA,CAAA;AAO1C"}
@@ -25,6 +25,7 @@ type TreeContextValue = {
25
25
  unregister: (id: string, element: HTMLDivElement | undefined) => void
26
26
  focusItem: (id: string) => void
27
27
  noteFocus: (id: string) => void
28
+ noteBlur: (next: EventTarget | null) => void
28
29
  activate: (id: string, event: Event) => void
29
30
  expand: (id: string, expanded: boolean) => void
30
31
  disclosure: () => TreeDisclosure
@@ -114,8 +115,14 @@ export type TreeProps = Omit<
114
115
  * the target row: when the active row leaves `visibleItems`, the first visible
115
116
  * row becomes the roving stop and `onActiveIdChange` is called with it. If the
116
117
  * removed row held focus, or was the last row the tree saw focused, focus moves
117
- * to that same row; if it had no focus, only the roving stop moves. This is the
118
- * same in both `disclosure` modes and for every way a host collapses a row.
118
+ * to that same row; if it had no focus, only the roving stop moves. Focus that
119
+ * has already gone to a control outside the tree is never pulled back: the tree
120
+ * forgets its last focused row when focus moves to an element outside it, and
121
+ * the roving stop alone moves if that control still has focus when the row is
122
+ * hidden. Focus the tree has asked for but not yet moved (a queued recovery, or a
123
+ * keyboard move to a row the host has not mounted) is withdrawn when focus lands
124
+ * outside the tree, and is checked again when a row mounts. This is the same in
125
+ * both `disclosure` modes and for every way a host collapses a row.
119
126
  */
120
127
  export function Tree(props: TreeProps) {
121
128
  const [local, rest] = splitProps(props, [
@@ -143,9 +150,26 @@ export function Tree(props: TreeProps) {
143
150
  const rows = new Map<string, HTMLDivElement>()
144
151
  const rowIdsByElement = new WeakMap<HTMLDivElement, string>()
145
152
  let pendingFocusId: string | null = null
153
+ let pendingFocusGuard: ((event: FocusEvent) => void) | undefined
146
154
  let requestedRevealId: string | null = null
147
155
  let focusedId: string | null = null
148
156
  let disposed = false
157
+ const isInsideTreeRow = (element: Element) => {
158
+ const row = element.closest('[role="treeitem"]')
159
+ return row instanceof HTMLDivElement && rowIdsByElement.has(row)
160
+ }
161
+ // True when a focusable control outside this tree owns focus. Nothing focused
162
+ // (`body`) is not outside: that is where focus falls when a focused row is removed.
163
+ const focusIsOutsideTree = () => {
164
+ if (typeof document === 'undefined') return false
165
+ const active = document.activeElement
166
+ return (
167
+ active instanceof Element &&
168
+ active !== document.body &&
169
+ active !== document.documentElement &&
170
+ !isInsideTreeRow(active)
171
+ )
172
+ }
149
173
  const rowSizeObserver =
150
174
  typeof ResizeObserver === 'undefined'
151
175
  ? undefined
@@ -160,8 +184,33 @@ export function Tree(props: TreeProps) {
160
184
  }
161
185
  })
162
186
 
187
+ // Focus the tree has asked for but not yet moved (a microtask away, or waiting for a
188
+ // virtualised row to mount) is only a request. While one is pending, focus arriving
189
+ // on a control outside the tree withdraws it, so a row mounting later cannot take
190
+ // focus back from a control the user, or the host, has since chosen.
191
+ const setPendingFocus = (id: string | null) => {
192
+ pendingFocusId = id
193
+ if (typeof document === 'undefined') return
194
+ if (id === null) {
195
+ if (pendingFocusGuard) document.removeEventListener('focusin', pendingFocusGuard, true)
196
+ pendingFocusGuard = undefined
197
+ } else if (!pendingFocusGuard) {
198
+ pendingFocusGuard = (event) => {
199
+ if (event.target instanceof Element && !isInsideTreeRow(event.target)) cancelPendingFocus()
200
+ }
201
+ document.addEventListener('focusin', pendingFocusGuard, true)
202
+ }
203
+ }
204
+ // Withdrawing a request also drops the memory of the row it was recovering for:
205
+ // nothing is left to recover.
206
+ const cancelPendingFocus = () => {
207
+ setPendingFocus(null)
208
+ focusedId = null
209
+ }
210
+
163
211
  onCleanup(() => {
164
212
  disposed = true
213
+ setPendingFocus(null)
165
214
  rowSizeObserver?.disconnect()
166
215
  })
167
216
 
@@ -176,13 +225,19 @@ export function Tree(props: TreeProps) {
176
225
  if (pendingFocusId !== id || effectiveActiveId() !== id) return
177
226
  const row = rows.get(id)
178
227
  if (!row || typeof document === 'undefined') return
228
+ // Ownership is checked when focus would move, not when it was requested: a
229
+ // control outside the tree may have taken focus since, and keeps it.
230
+ if (focusIsOutsideTree()) {
231
+ cancelPendingFocus()
232
+ return
233
+ }
179
234
  row.focus()
180
- if (document.activeElement === row) pendingFocusId = null
235
+ if (document.activeElement === row) setPendingFocus(null)
181
236
  })
182
237
  }
183
238
 
184
239
  const focusItem = (id: string) => {
185
- pendingFocusId = id
240
+ setPendingFocus(id)
186
241
  local.onActiveIdChange(id)
187
242
  if (rows.has(id)) requestedRevealId = null
188
243
  else requestReveal(id)
@@ -216,8 +271,14 @@ export function Tree(props: TreeProps) {
216
271
  if (focusWasInside) {
217
272
  queueMicrotask(() => {
218
273
  const fallbackId = effectiveActiveId()
219
- if (!disposed && fallbackId !== null && pendingFocusId !== fallbackId)
220
- focusItem(fallbackId)
274
+ if (disposed || fallbackId === null || pendingFocusId === fallbackId) return
275
+ // Focus fell out of the removed row, but it may have been given to a control
276
+ // outside the tree before this ran. Only nothing-focused (body) recovers.
277
+ if (focusIsOutsideTree()) {
278
+ focusedId = null
279
+ return
280
+ }
281
+ focusItem(fallbackId)
221
282
  })
222
283
  }
223
284
  },
@@ -225,6 +286,13 @@ export function Tree(props: TreeProps) {
225
286
  noteFocus: (id) => {
226
287
  focusedId = id
227
288
  },
289
+ noteBlur: (next) => {
290
+ // Focus moving to an element outside this tree ends the memory of the last
291
+ // focused row and withdraws any focus the tree was still waiting to give a
292
+ // row. A null target (focus to nothing, or the window losing focus) is
293
+ // ambiguous, so both stay for the recovery below.
294
+ if (next instanceof Element && !isInsideTreeRow(next)) cancelPendingFocus()
295
+ },
228
296
  activate: (id, event) => local.onActivate?.(id, event),
229
297
  expand: (id, expanded) => local.onExpand?.(id, expanded),
230
298
  disclosure: () => local.disclosure ?? 'row',
@@ -239,11 +307,15 @@ export function Tree(props: TreeProps) {
239
307
  if (activeId === null) return
240
308
  // If the projection removes the last row that took focus (a host filter, or
241
309
  // an ancestor collapsing), move focus to the same fallback that now owns the
242
- // roving tab stop. `focusedId` is not cleared when focus leaves the tree, so
243
- // this also reclaims focus from a control outside it; that long-standing
244
- // behaviour is deliberately left as it was.
310
+ // roving tab stop, unless a control outside the tree has focus: that control
311
+ // keeps it, and only the roving stop moves.
245
312
  if (focusedId !== null && !index().byId.has(focusedId) && pendingFocusId !== activeId) {
246
- focusItem(activeId)
313
+ if (focusIsOutsideTree()) {
314
+ focusedId = null
315
+ if (local.activeId !== activeId) local.onActiveIdChange(activeId)
316
+ } else {
317
+ focusItem(activeId)
318
+ }
247
319
  } else if (local.activeId !== activeId) {
248
320
  local.onActiveIdChange(activeId)
249
321
  }
@@ -354,12 +426,19 @@ export function TreeRow(props: TreeRowProps) {
354
426
  let registeredId: string | undefined
355
427
  const id = () => local.item.id
356
428
 
357
- onCleanup(() => tree.unregister(registeredId ?? id(), row))
429
+ const handleFocusOut = (event: FocusEvent) => tree.noteBlur(event.relatedTarget)
430
+
431
+ onCleanup(() => {
432
+ row?.removeEventListener('focusout', handleFocusOut)
433
+ tree.unregister(registeredId ?? id(), row)
434
+ })
358
435
 
359
436
  const setRef = (element: HTMLDivElement) => {
360
437
  row = element
361
438
  registeredId = id()
362
439
  tree.register(registeredId, element)
440
+ // A listener, not an `onFocusOut` prop, so a host's own handler is untouched.
441
+ element.addEventListener('focusout', handleFocusOut)
363
442
  }
364
443
 
365
444
  createEffect(() => {
@@ -12,6 +12,12 @@ import { cn } from '../../../lib/utils'
12
12
  * narrow screens the middle track yields space to the controls, and each side
13
13
  * group scrolls when necessary so keyboard focus can reveal every action.
14
14
  *
15
+ * The centre track is sized for a lone search field. A bar whose centre holds
16
+ * more than that, such as a search plus an action beside it, passes
17
+ * `centerWidth="wide"`: the centre then stays fluid below 1280px, never
18
+ * narrower than 7rem so its controls are not squeezed out by a long leading
19
+ * group (which scrolls instead), and takes a wider fixed track above it.
20
+ *
15
21
  * On a frameless desktop window this row is also the drag region, so it carries
16
22
  * `window-drag` and the controls inside it opt back out. `macos-inset` and
17
23
  * `windows-inset` reserve the space the native window controls occupy, which is
@@ -27,6 +33,13 @@ export type TopBarProps = ComponentProps<'header'> & {
27
33
  windowsInset?: boolean
28
34
  /** Blur the content scrolling under the bar. */
29
35
  glass?: boolean
36
+ /**
37
+ * How wide the centre track is. `field` (default) suits a lone search field.
38
+ * `wide` suits a centre that also holds an action: fluid below 1280px and
39
+ * never narrower than 7rem (the side groups scroll to make room), then a
40
+ * clamp of 32vw up to 672px between equal side tracks.
41
+ */
42
+ centerWidth?: 'field' | 'wide'
30
43
  }
31
44
 
32
45
  export function TopBar(props: TopBarProps) {
@@ -36,6 +49,7 @@ export function TopBar(props: TopBarProps) {
36
49
  'macosInset',
37
50
  'windowsInset',
38
51
  'glass',
52
+ 'centerWidth',
39
53
  ])
40
54
 
41
55
  return (
@@ -43,6 +57,8 @@ export function TopBar(props: TopBarProps) {
43
57
  data-slot="top-bar"
44
58
  class={cn(
45
59
  'h-topbar relative z-(--z-sticky) grid shrink-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-3 border-b border-border bg-card text-card-foreground md:grid-cols-[minmax(0,1fr)_clamp(240px,22vw,480px)_minmax(0,1fr)]',
60
+ local.centerWidth === 'wide' &&
61
+ 'grid-cols-[auto_minmax(7rem,1fr)_auto] gap-2 md:grid-cols-[auto_minmax(7rem,1fr)_auto] xl:grid-cols-[minmax(0,1fr)_clamp(240px,32vw,672px)_minmax(0,1fr)]',
46
62
  local.glass && 'glass-panel',
47
63
  local.draggable && 'window-drag',
48
64
  /* Each edge gets exactly one inline-padding utility. A window inset
@@ -1,5 +1,5 @@
1
1
  import type { ComponentProps } from 'solid-js'
2
- import { splitProps } from 'solid-js'
2
+ import { children, createMemo, splitProps } from 'solid-js'
3
3
  import { cva, type VariantProps } from '../../../lib/variants'
4
4
  import { cn } from '../../../lib/utils'
5
5
 
@@ -75,15 +75,35 @@ export const calloutVariants = cva(
75
75
 
76
76
  export type CalloutProps = ComponentProps<'div'> & VariantProps<typeof calloutVariants>
77
77
 
78
+ const isRendered = (node: unknown) =>
79
+ node != null && typeof node !== 'boolean' && (typeof node !== 'string' || node.trim() !== '')
80
+
81
+ const isCalloutTitle = (node: unknown) =>
82
+ typeof node === 'object' &&
83
+ node !== null &&
84
+ 'getAttribute' in node &&
85
+ typeof node.getAttribute === 'function' &&
86
+ node.getAttribute('data-slot') === 'callout-title'
87
+
78
88
  export function Callout(props: CalloutProps) {
79
- const [local, rest] = splitProps(props, ['class', 'variant', 'size'])
89
+ const [local, rest] = splitProps(props, ['class', 'variant', 'size', 'children'])
90
+ const resolved = children(() => local.children)
91
+ // CSS cannot see text nodes, so the callout looks at what it renders and marks a
92
+ // title with nothing after it. The title reads the mark to drop its bottom margin.
93
+ const titleOnly = createMemo(() => {
94
+ const nodes = resolved.toArray().filter(isRendered)
95
+ return nodes.length === 1 && isCalloutTitle(nodes[0])
96
+ })
80
97
  return (
81
98
  <div
82
99
  data-slot="callout"
100
+ data-title-only={titleOnly() ? '' : undefined}
83
101
  role="note"
84
102
  class={cn(calloutVariants({ variant: local.variant, size: local.size }), local.class)}
85
103
  {...rest}
86
- />
104
+ >
105
+ {resolved()}
106
+ </div>
87
107
  )
88
108
  }
89
109
 
@@ -93,22 +113,23 @@ export function Callout(props: CalloutProps) {
93
113
  * level would break it. Pass `role="heading"` with `aria-level` where the
94
114
  * content does belong in the outline.
95
115
  *
96
- * The title always carries a 4px (`mb-1`) margin below it, whatever follows: a
116
+ * The title carries a 4px (`mb-1`) margin below it, whatever follows: a
97
117
  * paragraph, a list, or bare text. CSS cannot see text nodes, so a selector such
98
118
  * as `:last-child` or `:only-child` cannot tell `<Title/>text` (needs the gap)
99
119
  * from `<Title/>` (needs none), and `not-last:mb-1` silently dropped the gap for
100
- * bare text, the most common body. The unconditional margin is the less
101
- * surprising failure: it is never missing where text follows. The price is the
102
- * title-only callout, which would end 4px taller than its padding says. Pass
103
- * `class="mb-0"` there (`cn` lets the caller's margin win); the `WithTitle` story
104
- * shows it.
120
+ * bare text, the most common body. So the gap stays the default and the one case
121
+ * that does not want it is detected by `Callout`, which sees its rendered
122
+ * children: a callout whose only child is a title carries `data-title-only`, and
123
+ * the title drops its margin so the block ends at its own padding. A caller never
124
+ * passes `mb-0` for this. Anything else after the title, text included, keeps the
125
+ * gap.
105
126
  */
106
127
  export function CalloutTitle(props: ComponentProps<'div'>) {
107
128
  const [local, rest] = splitProps(props, ['class'])
108
129
  return (
109
130
  <div
110
131
  data-slot="callout-title"
111
- class={cn('mb-1 font-medium tracking-tight', local.class)}
132
+ class={cn('mb-1 font-medium tracking-tight in-data-title-only:mb-0', local.class)}
112
133
  {...rest}
113
134
  />
114
135
  )
@@ -1,4 +1,4 @@
1
- import { splitProps, useContext, type ComponentProps, type JSX } from 'solid-js'
1
+ import { Show, splitProps, useContext, type ComponentProps, type JSX } from 'solid-js'
2
2
  import { ActionButton } from '../../composites/action-button'
3
3
  import { FormFieldContext } from '../../../lib/form-field'
4
4
  import { InputGroup, InputGroupInput } from './input-group'
@@ -56,28 +56,31 @@ export function SecretInputGroup(props: SecretInputGroupProps) {
56
56
  />
57
57
  )
58
58
 
59
- if (!local.onClear) return <InputGroup class={local.class}>{input}</InputGroup>
60
-
59
+ // One group for both cases: `onClear` may appear or disappear after setup
60
+ // (a stored secret is cleared, or a configured one arrives), and the input
61
+ // must keep its element, value and focus while the action comes and goes.
61
62
  return (
62
63
  <InputGroup class={local.class}>
63
64
  {input}
64
- <ActionButton
65
- data-slot="input-group-button"
66
- variant="ghost"
67
- size="xs"
68
- class="-me-1 h-6 min-w-0 shrink-0 px-2"
69
- disabled={local.disabled}
70
- tooltip={
71
- local.disabled
72
- ? (local.disabledReason ?? 'This API key is managed by policy')
73
- : (local.clearLabel ?? 'Clear API key')
74
- }
75
- tooltipSide="top"
76
- aria-label={local.clearLabel ?? 'Clear API key'}
77
- onClick={() => local.onClear?.()}
78
- >
79
- Clear
80
- </ActionButton>
65
+ <Show when={local.onClear}>
66
+ <ActionButton
67
+ data-slot="input-group-button"
68
+ variant="ghost"
69
+ size="xs"
70
+ class="-me-1 h-6 min-w-0 shrink-0 px-2"
71
+ disabled={local.disabled}
72
+ tooltip={
73
+ local.disabled
74
+ ? (local.disabledReason ?? 'This API key is managed by policy')
75
+ : (local.clearLabel ?? 'Clear API key')
76
+ }
77
+ tooltipSide="top"
78
+ aria-label={local.clearLabel ?? 'Clear API key'}
79
+ onClick={() => local.onClear?.()}
80
+ >
81
+ Clear
82
+ </ActionButton>
83
+ </Show>
81
84
  </InputGroup>
82
85
  )
83
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.138.0",
3
+ "version": "0.139.0",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",