@dungsil/dsh-plugin-ui-model-picker 0.2.0 → 0.2.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.
@@ -1,4 +1,6 @@
1
1
  Chevron SVG artwork from @deepseek-ai/dsh-client-ui-primitives 0.2.1-rc.5
2
+ Menu transition compatibility styles adapted from
3
+ @deepseek-ai/dsh-client-ui-primitives 0.2.2-alpha.3 (MenuSurface.module.css)
2
4
 
3
5
  MIT License
4
6
 
Binary file
package/dist/client.js CHANGED
@@ -2,59 +2,53 @@ window.__ModuleLoader__.load({ id: "@dungsil/dsh-plugin-ui-model-picker", factor
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  let react = require("react");
4
4
  let react_dom = require("react-dom");
5
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
5
6
  let react_jsx_runtime = require("react/jsx-runtime");
6
- // src/chevron.tsx
7
- const paths = {
8
- down: "M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6",
9
- left: "M10 4L6.70711 7.29289C6.31658 7.68342 6.31658 8.31658 6.70711 8.70711L10 12",
10
- right: "M6 12L9.29289 8.70711C9.68342 8.31658 9.68342 7.68342 9.29289 7.29289L6 4"
11
- };
12
- function Chevron({ direction }) {
13
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
14
- className: "dsh-codex-chevron",
15
- width: "14",
16
- height: "14",
17
- viewBox: "0 0 16 16",
18
- fill: "none",
19
- strokeWidth: "1",
20
- xmlns: "http://www.w3.org/2000/svg",
21
- "aria-hidden": "true",
22
- focusable: "false",
23
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
24
- d: paths[direction],
25
- stroke: "currentColor"
26
- })
27
- });
28
- }
29
7
  // src/styles.ts
30
8
  const styles = `
31
- .dsh-codex-picker { display: flex; align-items: center; min-width: 0; border-radius: 16px; color: var(--dsw-alias-label-primary, #252525); font: 13px/20px system-ui, sans-serif; }
32
- .dsh-codex-picker button { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; height: 28px; padding: 0 6px; border-radius: 14px; min-width: 0; }
33
- .dsh-codex-picker:hover { background: var(--dsw-alias-interactive-bg-hover, #8881); }
34
- .dsh-codex-picker .dsh-codex-trigger { display: flex; gap: 5px; align-items: center; }
35
- .dsh-codex-chevron { display: inline-block; flex: 0 0 14px; vertical-align: middle; }
36
- .dsh-codex-name { display: var(--dsh-composer-model-text-display, block); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: min(220px, 32vw); }
37
- .dsh-codex-effort { color: var(--dsw-alias-label-tertiary, #777); }
38
- .dsh-codex-popover { position: fixed; inset: auto; margin: 0; box-sizing: border-box; width: 256px; max-width: calc(100vw - 16px); max-height: calc(100dvh - 16px); overflow-y: auto; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l1, #e8e8e8); border-radius: 16px; background: var(--dsw-alias-bg-layer-1, #fff); color: var(--dsw-alias-label-primary, #252525); box-shadow: 0 5px 12px #0000000d, 0 2px 4px #00000005; font: 13px/20px system-ui, sans-serif; }
39
- .dsh-codex-popover::backdrop { background: transparent; }
9
+ .dsh-codex-picker { display: flex; align-items: center; min-width: 0; }
10
+ .dsh-codex-trigger { display: flex; align-items: center; gap: 4px; min-width: 0; max-width: min(360px, 45cqw); height: 28px; padding: 0 4px 0 8px; border: none; border-radius: var(--dsw-radius-sm); background: transparent; color: var(--dsw-alias-label-secondary); font-family: inherit; font-size: 13px; font-weight: 400; line-height: 20px; cursor: pointer; outline: none; }
11
+ .dsh-codex-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
12
+ .dsh-codex-trigger:focus-visible:not([data-dsh-automatic-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
13
+ .dsh-codex-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
14
+ .dsh-codex-trigger-icon { display: var(--dsh-composer-model-icon-display, none); flex: none; }
15
+ .dsh-codex-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform .12s; }
16
+ .dsh-codex-trigger[aria-expanded=true] .dsh-codex-chevron { transform: rotate(180deg); }
17
+ .dsh-codex-name, .dsh-codex-effort { display: var(--dsh-composer-model-text-display, block); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
18
+ .dsh-codex-effort { color: var(--dsw-alias-label-caption); flex-shrink: 1000; }
19
+ .dsh-codex-popover { position: fixed; z-index: 1100; display: flex; flex-direction: column; inset: auto; margin: 0; box-sizing: border-box; width: 256px; max-width: calc(100vw - 32px); max-height: min(360px, calc(100dvh - 96px)); overflow: hidden; padding: 4px; border: 0; --dsw-elevation-stroke-color: var(--dsw-alias-border-l1); --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2); --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2); background: transparent; color: var(--dsw-alias-label-primary); box-shadow: var(--dsw-elevation-prominent); font: 13px/20px var(--dsw-font-family, system-ui); }
20
+ /* rc.5 has no MenuSurface phase API. Match the newer shared menu fades without
21
+ animating the container: container opacity would change its backdrop blur.
22
+ Old hosts forward phase to the div; newer hosts consume it for their own CSS. */
23
+ .dsh-codex-popover[phase=enter] > * { animation: dsh-codex-menu-in 40ms ease-out; }
24
+ .dsh-codex-popover[phase=dismiss] > * { animation: dsh-codex-menu-out 80ms ease-in both; }
25
+ @keyframes dsh-codex-menu-in { from { opacity: 0; } to { opacity: 1; } }
26
+ @keyframes dsh-codex-menu-out { from { opacity: 1; } to { opacity: 0; } }
27
+ @media (prefers-reduced-motion: reduce) {
28
+ .dsh-codex-popover[data-picker-phase] > * { animation-duration: 0ms; }
29
+ .dsh-codex-chevron { transition: none; }
30
+ }
31
+ .dsh-codex-popover > :not([role=radiogroup]) { flex-shrink: 0; }
32
+ .dsh-codex-popover [role=radiogroup] { min-height: 0; overflow-y: auto; }
40
33
  .dsh-codex-header { display: grid; grid-template-columns: 24px minmax(0, 1fr) 24px; align-items: start; gap: 4px; }
41
- .dsh-codex-model, .dsh-codex-reset, .dsh-codex-back { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; border-radius: 6px; }
34
+ .dsh-codex-model, .dsh-codex-reset, .dsh-codex-back { border: 0; background: transparent; color: inherit; font: inherit; cursor: pointer; border-radius: var(--dsw-radius-md); }
35
+ .dsh-codex-popover svg { flex: none; vertical-align: middle; }
42
36
  .dsh-codex-model { grid-column: 2; justify-self: center; min-width: 0; max-width: 100%; padding: 2px 10px 3px; text-align: center; }
43
- .dsh-codex-model strong { display: block; color: #3983ff; font-size: 14px; line-height: 20px; font-weight: 600; }
37
+ .dsh-codex-model strong { display: block; color: var(--dsw-alias-state-business-primary); font-size: 14px; line-height: 20px; font-weight: 600; }
44
38
  .dsh-codex-model > span { display: block; color: var(--dsw-alias-label-tertiary, #777); font-size: 12px; line-height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
45
39
  .dsh-codex-model:hover, .dsh-codex-reset:hover, .dsh-codex-back:hover { background: var(--dsw-alias-interactive-bg-hover, #f2f2f2); }
46
40
  .dsh-codex-reset { display: grid; place-items: center; width: 24px; height: 24px; color: var(--dsw-alias-label-tertiary, #999); }
47
41
  .dsh-codex-back { padding: 4px 8px; color: var(--dsw-alias-label-tertiary, #777); }
48
42
  .dsh-codex-popover h2, .dsh-codex-provider { margin: 0; padding: 6px 10px; font-size: 12px; font-weight: 400; color: var(--dsw-alias-label-tertiary, #777); }
49
43
  .dsh-codex-provider { font-size: 11px; padding-top: 10px; }
50
- .dsh-codex-option { display: flex; align-items: center; justify-content: space-between; gap: 12px; width: 100%; min-height: 30px; padding: 5px 10px; border: 0; border-radius: 9px; color: inherit; background: transparent; text-align: left; font: inherit; cursor: pointer; }
51
- .dsh-codex-option:hover, .dsh-codex-option[aria-checked=true] { background: var(--dsw-alias-interactive-bg-hover, #f4f4f4); }
44
+ .dsh-codex-option { box-sizing: border-box; display: flex; align-items: center; justify-content: space-between; gap: 6px; width: 100%; min-height: 34px; padding: 5px 7px; border: 0; border-radius: var(--dsw-radius-md); color: inherit; background: transparent; text-align: left; font: inherit; cursor: pointer; }
45
+ .dsh-codex-option:hover:not(:disabled), .dsh-codex-option:focus-visible { background: var(--dsw-alias-interactive-bg-hover); }
52
46
  .dsh-codex-option span:first-child { min-width: 0; overflow-wrap: anywhere; }
53
47
  .dsh-codex-option small { display: block; color: var(--dsw-alias-label-tertiary, #777); font-size: 11px; }
54
- .dsh-codex-option .dsh-codex-check { flex: 0 0 14px; }
48
+ .dsh-codex-option .dsh-codex-check { display: grid; place-items: center; flex: 0 0 14px; }
55
49
  .dsh-codex-slider { padding: 10px 2px 3px; }
56
50
  .dsh-codex-rail { position: relative; height: 28px; }
57
- .dsh-codex-track { position: absolute; inset: 3px 0; border-radius: 20px; background: linear-gradient(to right, #3983ff var(--dsh-codex-progress), var(--dsw-alias-interactive-bg-hover, #e8e8e8) var(--dsh-codex-progress)); display: flex; align-items: center; justify-content: space-between; padding: 0 12px; }
51
+ .dsh-codex-track { position: absolute; inset: 3px 0; border-radius: 20px; background: linear-gradient(to right, var(--dsw-alias-state-business-primary) var(--dsh-codex-progress), var(--dsw-alias-interactive-bg-hover) var(--dsh-codex-progress)); display: flex; align-items: center; justify-content: space-between; padding: 0 12px; }
58
52
  .dsh-codex-track span { width: 4px; height: 4px; border-radius: 50%; background: #b8b8b8; }
59
53
  .dsh-codex-track span[data-filled=true] { background: #ffffff80; }
60
54
  .dsh-codex-slider input { appearance: none; -webkit-appearance: none; position: relative; display: block; width: 100%; height: 28px; padding: 0; margin: 0; background: transparent; cursor: pointer; }
@@ -64,8 +58,8 @@ const styles = `
64
58
  .dsh-codex-slider input::-moz-range-thumb { width: 26px; height: 26px; border: 1px solid #0000000a; border-radius: 50%; background: #fff; box-shadow: 0 1px 3px #0002; }
65
59
  .dsh-codex-notice { padding: 6px 10px; margin: 0; font-size: 12px; overflow-wrap: anywhere; }
66
60
  .dsh-codex-popover [role=alert] { color: var(--dsw-alias-state-error-primary, #b54424); }
67
- .dsh-codex-picker button:focus-visible, .dsh-codex-popover :focus-visible { outline: 2px solid var(--dsw-focus-ring-color, #3186ce); outline-offset: 2px; }
68
- .dsh-codex-picker button:disabled, .dsh-codex-popover :disabled { opacity: .5; cursor: default; }
61
+ .dsh-codex-popover :focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: -2px; }
62
+ .dsh-codex-popover :disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
69
63
  `;
70
64
  // src/client.tsx
71
65
  const name = "dungsil-ui-model-picker";
@@ -127,6 +121,7 @@ function apply(ctx) {
127
121
  const locale = (0, react.useSyncExternalStore)((fn) => ctx.locale.subscribe(fn), () => ctx.locale.getSnapshot().active);
128
122
  const text = copy[locale.startsWith("ko") ? "ko" : locale.startsWith("zh") ? "zh" : "en"];
129
123
  const [panel, setPanel] = (0, react.useState)(null);
124
+ const [exiting, setExiting] = (0, react.useState)(false);
130
125
  const [draft, setDraft] = (0, react.useState)(0);
131
126
  const [failed, setFailed] = (0, react.useState)(false);
132
127
  const trigger = (0, react.useRef)(null);
@@ -155,14 +150,30 @@ function apply(ctx) {
155
150
  };
156
151
  }, [directory]);
157
152
  const close = () => {
158
- setPanel(null);
159
- trigger.current?.focus();
153
+ setExiting(true);
154
+ if (trigger.current) (0, _deepseek_ai_dsh_client_ui_primitives.focusWithoutRing)(trigger.current);
160
155
  };
161
- (0, react.useEffect)(() => {
156
+ (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(anchor, panel !== null && !exiting, () => setExiting(true), popup);
157
+ (0, react.useLayoutEffect)(() => {
158
+ if (!exiting || !popup.current) return;
159
+ const element = popup.current;
160
+ element.inert = true;
161
+ let cancelled = false;
162
+ const animations = element.getAnimations({ subtree: true }).filter((animation) => {
163
+ const name = animation.animationName;
164
+ return name?.includes("dsh-context-menu-out") || name === "dsh-codex-menu-out";
165
+ });
166
+ Promise.all(animations.map((animation) => animation.finished.catch(() => {}))).then(() => {
167
+ if (!cancelled) setPanel(null);
168
+ });
169
+ return () => {
170
+ cancelled = true;
171
+ element.inert = false;
172
+ };
173
+ }, [exiting]);
174
+ (0, react.useLayoutEffect)(() => {
162
175
  if (!panel || !popup.current || !anchor.current) return;
163
176
  const element = popup.current;
164
- element.setAttribute("popover", "auto");
165
- element.showPopover();
166
177
  const position = () => {
167
178
  const box = anchor.current.getBoundingClientRect();
168
179
  element.style.left = `${Math.max(8, Math.min(box.right - element.offsetWidth, window.innerWidth - element.offsetWidth - 8))}px`;
@@ -172,13 +183,9 @@ function apply(ctx) {
172
183
  position();
173
184
  const observer = new ResizeObserver(position);
174
185
  observer.observe(element);
175
- const toggle = (event) => {
176
- if (event.newState === "closed") setPanel(null);
177
- };
178
186
  const scroll = (event) => {
179
187
  if (!element.contains(event.target)) close();
180
188
  };
181
- element.addEventListener("toggle", toggle);
182
189
  window.addEventListener("resize", close);
183
190
  window.addEventListener("scroll", scroll, true);
184
191
  const keydown = (event) => {
@@ -206,15 +213,13 @@ function apply(ctx) {
206
213
  window.removeEventListener("resize", close);
207
214
  window.removeEventListener("scroll", scroll, true);
208
215
  element.removeEventListener("keydown", keydown);
209
- element.removeEventListener("toggle", toggle);
210
- element.hidePopover();
211
216
  };
212
217
  }, [panel]);
213
218
  (0, react.useEffect)(() => {
214
- if (locked) setPanel(null);
219
+ if (locked) setExiting(true);
215
220
  }, [locked]);
216
221
  (0, react.useEffect)(() => {
217
- setPanel(null);
222
+ setExiting(true);
218
223
  }, [
219
224
  current?.provider,
220
225
  current?.model,
@@ -237,9 +242,14 @@ function apply(ctx) {
237
242
  };
238
243
  const show = (button) => {
239
244
  trigger.current = button;
245
+ if (panel && !exiting) {
246
+ close();
247
+ return;
248
+ }
249
+ setExiting(false);
240
250
  setDraft(Math.max(0, effortIndex));
241
251
  setFailed(false);
242
- setPanel(panel ? null : "effort");
252
+ setPanel("effort");
243
253
  directory.load().catch(() => {});
244
254
  };
245
255
  const selectEffort = (index) => {
@@ -260,22 +270,34 @@ function apply(ctx) {
260
270
  className: "dsh-codex-trigger",
261
271
  disabled,
262
272
  "aria-label": `${text.choose}: ${model?.name ?? current?.model ?? text.model}${effortLabel ? `, ${effortLabel}` : ""}`,
273
+ title: `${model?.name ?? current?.model ?? text.model}${effortLabel ? ` ${effortLabel}` : ""}`,
263
274
  "aria-haspopup": "dialog",
264
- "aria-expanded": panel !== null,
265
- "aria-controls": panel ? id : void 0,
275
+ "aria-expanded": panel !== null && !exiting,
276
+ "aria-controls": panel && !exiting ? id : void 0,
266
277
  onClick: (event) => show(event.currentTarget),
267
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
268
- className: "dsh-codex-name",
269
- children: panel ? text.choose : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [model?.name ?? current?.model ?? text.model, effortLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [" ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
278
+ children: [
279
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDataOutlineRegular, {
280
+ className: "dsh-codex-trigger-icon",
281
+ size: 16
282
+ }),
283
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
284
+ className: "dsh-codex-name",
285
+ children: model?.name ?? current?.model ?? text.model
286
+ }),
287
+ effortLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
270
288
  className: "dsh-codex-effort",
271
289
  children: effortLabel
272
- })] })] })
273
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, { direction: "down" })]
290
+ }),
291
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: "dsh-codex-chevron" })
292
+ ]
274
293
  })
275
- }), panel && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
294
+ }), panel && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.MenuSurface, {
276
295
  id,
277
296
  ref: popup,
278
297
  role: "dialog",
298
+ "aria-hidden": exiting || void 0,
299
+ "data-picker-phase": exiting ? "dismiss" : "enter",
300
+ phase: exiting ? "dismiss" : "enter",
279
301
  "aria-label": panel === "model" ? text.model : text.effort,
280
302
  className: "dsh-codex-popover",
281
303
  children: [
@@ -284,7 +306,7 @@ function apply(ctx) {
284
306
  className: "dsh-codex-back",
285
307
  onClick: () => setPanel("effort"),
286
308
  children: [
287
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, { direction: "left" }),
309
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronLeftOutlineRegular, { size: 14 }),
288
310
  " ",
289
311
  text.effort
290
312
  ]
@@ -299,7 +321,7 @@ function apply(ctx) {
299
321
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: efforts[draft]?.name ?? effortLabel ?? text.default }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
300
322
  model?.name ?? current?.model ?? text.model,
301
323
  " ",
302
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Chevron, { direction: "right" })
324
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronRightOutlineRegular, { size: 14 })
303
325
  ] })]
304
326
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
305
327
  type: "button",
@@ -376,7 +398,7 @@ function apply(ctx) {
376
398
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [option.name, option.description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: option.description })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
377
399
  className: "dsh-codex-check",
378
400
  "aria-hidden": "true",
379
- children: provider.id === current?.provider && option.id === current?.model ? "✓" : ""
401
+ children: provider.id === current?.provider && option.id === current?.model && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutlineRegular, { size: 14 })
380
402
  })]
381
403
  }, option.id))]
382
404
  }, provider.id))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dungsil/dsh-plugin-ui-model-picker",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Model and reasoning effort picker for the DeepSeek Harness composer.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,6 +32,7 @@
32
32
  "@deepseek-ai/dsh-client-store": "0.2.1-rc.5",
33
33
  "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.5",
34
34
  "@deepseek-ai/dsh-client-ui-model-selection": "0.2.1-rc.5",
35
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.5",
35
36
  "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-rc.5",
36
37
  "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.5",
37
38
  "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.5",
@@ -43,6 +44,9 @@
43
44
  "react-dom": "18.3.1",
44
45
  "tsdown": "0.23.4"
45
46
  },
47
+ "peerDependencies": {
48
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.2.1-rc.5"
49
+ },
46
50
  "engines": {
47
51
  "dsh": "^0.2.1-rc.5",
48
52
  "node": ">=24.9.0"