@nucleux/templates 0.0.0-stage → 0.7.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.
- package/LICENSE +21 -0
- package/README.md +53 -2
- package/dist/index.cjs +171 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +147 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nucleux
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,54 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nucleux/templates
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Templates — fill-in-the-blank prompt scaffolds that turn vague intent into a good question — Nucleux agentic UI for React.
|
|
4
|
+
|
|
5
|
+
Part of **[Nucleux](https://github.com/intelligaia-open-labs/nucleux)** — agentic UI components for React. Install a single component, or the full kit via [`@nucleux/react`](https://www.npmjs.com/package/@nucleux/react).
|
|
6
|
+
|
|
7
|
+
Structured prompts that capture good prompting: a sentence with named slots,
|
|
8
|
+
a list of fully written prompts, or a library grouped by domain. Nothing runs
|
|
9
|
+
until the person picks "Use this prompt" or a template.
|
|
10
|
+
|
|
11
|
+
## Exports
|
|
12
|
+
|
|
13
|
+
- **`Templates`** — fill-in-the-blank (`fill-blanks`): a form with inline slot fields
|
|
14
|
+
- **`TemplatesList`** — prefilled list (`prefilled-list`): one-tap prompts
|
|
15
|
+
- **`TemplatesBrowser`** — categorised browser (`categorized-browser`): vertical tabs per category
|
|
16
|
+
- Types: `TemplatesProps`, `PromptSegment`, `PromptSlot`, `TemplatesListProps`, `TemplatesListItem`, `TemplatesBrowserProps`, `TemplatesCategory`
|
|
17
|
+
|
|
18
|
+
## Behaviour
|
|
19
|
+
|
|
20
|
+
- `segments` mix literal strings and slots `{ slot, placeholder, label?, options?, required? }`. `options` give native type-ahead; the field stays free text.
|
|
21
|
+
- Required slots are counted ("1 blank left", `aria-live`) and the submit waits for them. Empty optional slots fall back to their placeholder.
|
|
22
|
+
- `onComplete(prompt, values)` gets the interpolated sentence. `TemplatesList`/`TemplatesBrowser` call `onSelect(prompt, item[, category])`.
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { Templates, TemplatesList, TemplatesBrowser } from "@nucleux/templates";
|
|
28
|
+
|
|
29
|
+
<Templates
|
|
30
|
+
segments={[
|
|
31
|
+
"Summarise ", { slot: "meeting", placeholder: "meeting", options: ["the standup", "the board review"], required: true },
|
|
32
|
+
" in ", { slot: "length", placeholder: "3 bullets" },
|
|
33
|
+
" for ", { slot: "audience", placeholder: "execs" }, ".",
|
|
34
|
+
]}
|
|
35
|
+
onComplete={(prompt) => setDraft(prompt)}
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
<TemplatesList aria-label="Templates" items={["Draft a follow-up to the at-risk accounts"]} onSelect={setDraft} />
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pnpm add @nucleux/templates @nucleux/tokens
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`react` / `react-dom` are peer dependencies.
|
|
48
|
+
|
|
49
|
+
Requires the theme once in your app: `import "@nucleux/tokens/styles.css";`
|
|
50
|
+
and the Tailwind preset from `@nucleux/tokens/preset`.
|
|
51
|
+
|
|
52
|
+
## License
|
|
53
|
+
|
|
54
|
+
MIT © Nucleux
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var React = require('react');
|
|
5
|
+
var lucideReact = require('lucide-react');
|
|
6
|
+
var utils = require('@nucleux/utils');
|
|
7
|
+
var button = require('@nucleux/button');
|
|
8
|
+
var tabs = require('@nucleux/tabs');
|
|
9
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
10
|
+
|
|
11
|
+
function _interopNamespace(e) {
|
|
12
|
+
if (e && e.__esModule) return e;
|
|
13
|
+
var n = Object.create(null);
|
|
14
|
+
if (e) {
|
|
15
|
+
Object.keys(e).forEach(function (k) {
|
|
16
|
+
if (k !== 'default') {
|
|
17
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
18
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () { return e[k]; }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
n.default = e;
|
|
26
|
+
return Object.freeze(n);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
30
|
+
|
|
31
|
+
// src/index.tsx
|
|
32
|
+
var Templates = React__namespace.forwardRef(
|
|
33
|
+
({
|
|
34
|
+
segments,
|
|
35
|
+
values,
|
|
36
|
+
defaultValues = {},
|
|
37
|
+
onValuesChange,
|
|
38
|
+
onComplete,
|
|
39
|
+
submitLabel = "Use this prompt",
|
|
40
|
+
className,
|
|
41
|
+
...props
|
|
42
|
+
}, ref) => {
|
|
43
|
+
const [internal, setInternal] = React__namespace.useState(defaultValues);
|
|
44
|
+
const current = values ?? internal;
|
|
45
|
+
const baseId = React__namespace.useId();
|
|
46
|
+
const setSlot = (slot, val) => {
|
|
47
|
+
const next = { ...current, [slot]: val };
|
|
48
|
+
if (values === void 0) setInternal(next);
|
|
49
|
+
onValuesChange?.(next);
|
|
50
|
+
};
|
|
51
|
+
const interpolate = () => segments.map((s) => typeof s === "string" ? s : current[s.slot] || s.placeholder).join("");
|
|
52
|
+
const required = segments.filter((s) => typeof s !== "string" && !!s.required);
|
|
53
|
+
const missing = required.filter((s) => !(current[s.slot] ?? "").trim()).length;
|
|
54
|
+
const statusId = `${baseId}-status`;
|
|
55
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
56
|
+
"form",
|
|
57
|
+
{
|
|
58
|
+
ref,
|
|
59
|
+
onSubmit: (e) => {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
if (missing > 0) return;
|
|
62
|
+
onComplete?.(interpolate(), current);
|
|
63
|
+
},
|
|
64
|
+
className: utils.cn("flex flex-col gap-4 rounded-xl border border-border bg-background p-5", className),
|
|
65
|
+
...props,
|
|
66
|
+
children: [
|
|
67
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-base leading-loose text-foreground", children: segments.map((segment, i) => {
|
|
68
|
+
if (typeof segment === "string") return /* @__PURE__ */ jsxRuntime.jsx(React__namespace.Fragment, { children: segment }, i);
|
|
69
|
+
const val = current[segment.slot] ?? "";
|
|
70
|
+
const listId = segment.options?.length ? `${baseId}-${segment.slot}-options` : void 0;
|
|
71
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(React__namespace.Fragment, { children: [
|
|
72
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
73
|
+
"input",
|
|
74
|
+
{
|
|
75
|
+
type: "text",
|
|
76
|
+
value: val,
|
|
77
|
+
onChange: (e) => setSlot(segment.slot, e.target.value),
|
|
78
|
+
placeholder: segment.placeholder,
|
|
79
|
+
"aria-label": segment.label ?? segment.placeholder,
|
|
80
|
+
"aria-required": segment.required || void 0,
|
|
81
|
+
list: listId,
|
|
82
|
+
autoComplete: "off",
|
|
83
|
+
size: Math.max((val || segment.placeholder).length, 4),
|
|
84
|
+
className: utils.cn(
|
|
85
|
+
"mx-0.5 inline-block rounded-md border-b-2 border-dashed border-brand/50 bg-brand-muted/40 px-1 text-center text-brand placeholder:text-brand/60 focus:border-solid focus:border-brand focus:outline-none",
|
|
86
|
+
val && "border-solid bg-brand-muted"
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
),
|
|
90
|
+
listId && /* @__PURE__ */ jsxRuntime.jsx("datalist", { id: listId, children: segment.options.map((o) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: o }, o)) })
|
|
91
|
+
] }, i);
|
|
92
|
+
}) }),
|
|
93
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-3", children: [
|
|
94
|
+
required.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { id: statusId, "aria-live": "polite", className: "mr-auto text-sm text-muted-foreground", children: missing === 0 ? "Ready to use" : `${missing} ${missing === 1 ? "blank" : "blanks"} left` }),
|
|
95
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
96
|
+
button.Button,
|
|
97
|
+
{
|
|
98
|
+
type: "submit",
|
|
99
|
+
variant: "cta",
|
|
100
|
+
disabled: missing > 0,
|
|
101
|
+
"aria-describedby": required.length > 0 ? statusId : void 0,
|
|
102
|
+
children: submitLabel
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
] })
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
Templates.displayName = "Templates";
|
|
112
|
+
var toListItem = (item) => typeof item === "string" ? { title: item } : item;
|
|
113
|
+
var TemplatesList = React__namespace.forwardRef(
|
|
114
|
+
({ items, onSelect, className, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
115
|
+
"ul",
|
|
116
|
+
{
|
|
117
|
+
ref,
|
|
118
|
+
role: "list",
|
|
119
|
+
className: utils.cn("m-0 flex list-none flex-col gap-1 p-0", className),
|
|
120
|
+
...props,
|
|
121
|
+
children: items.map(toListItem).map((item) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
122
|
+
"button",
|
|
123
|
+
{
|
|
124
|
+
type: "button",
|
|
125
|
+
onClick: () => onSelect?.(item.prompt ?? item.title, item),
|
|
126
|
+
className: "flex w-full items-start gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
127
|
+
children: [
|
|
128
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowRight, { "aria-hidden": "true", className: "mt-0.5 size-4 shrink-0 text-muted-foreground" }),
|
|
129
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [
|
|
130
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: item.title }),
|
|
131
|
+
item.description && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: item.description })
|
|
132
|
+
] })
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
) }, item.id ?? item.title))
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
);
|
|
139
|
+
TemplatesList.displayName = "TemplatesList";
|
|
140
|
+
var TemplatesBrowser = React__namespace.forwardRef(
|
|
141
|
+
({ categories, category, defaultCategory, onCategoryChange, onSelect, className, ...props }, ref) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
142
|
+
tabs.Tabs,
|
|
143
|
+
{
|
|
144
|
+
ref,
|
|
145
|
+
orientation: "vertical",
|
|
146
|
+
value: category,
|
|
147
|
+
defaultValue: defaultCategory ?? categories[0]?.id ?? "",
|
|
148
|
+
onValueChange: onCategoryChange,
|
|
149
|
+
className: utils.cn("gap-3", className),
|
|
150
|
+
...props,
|
|
151
|
+
children: [
|
|
152
|
+
/* @__PURE__ */ jsxRuntime.jsx(tabs.TabsList, { "aria-label": "Template categories", className: "h-fit shrink-0 bg-transparent p-0", children: categories.map((c) => /* @__PURE__ */ jsxRuntime.jsx(tabs.TabsTrigger, { value: c.id, className: "justify-start aria-selected:bg-muted aria-selected:shadow-none", children: c.label }, c.id)) }),
|
|
153
|
+
categories.map((c) => /* @__PURE__ */ jsxRuntime.jsx(tabs.TabsContent, { value: c.id, className: "min-w-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
154
|
+
TemplatesList,
|
|
155
|
+
{
|
|
156
|
+
"aria-label": `${c.label} templates`,
|
|
157
|
+
items: c.templates,
|
|
158
|
+
onSelect: (prompt, item) => onSelect?.(prompt, item, c.id)
|
|
159
|
+
}
|
|
160
|
+
) }, c.id))
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
TemplatesBrowser.displayName = "TemplatesBrowser";
|
|
166
|
+
|
|
167
|
+
exports.Templates = Templates;
|
|
168
|
+
exports.TemplatesBrowser = TemplatesBrowser;
|
|
169
|
+
exports.TemplatesList = TemplatesList;
|
|
170
|
+
//# sourceMappingURL=index.cjs.map
|
|
171
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.tsx"],"names":["React","jsxs","cn","jsx","Button","ArrowRight","Tabs","TabsList","TabsTrigger","TabsContent"],"mappings":"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","file":"index.cjs","sourcesContent":["import * as React from \"react\";\nimport { ArrowRight } from \"lucide-react\";\nimport { cn } from \"@nucleux/utils\";\nimport { Button } from \"@nucleux/button\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@nucleux/tabs\";\n\n/** A fill-in-the-blank slot: an id, the placeholder shown while empty, and optional type-ahead options. */\nexport interface PromptSlot {\n /** Slot id — the key in `values`. */\n slot: string;\n /** Shown while the slot is empty, and used in the prompt if it stays empty. */\n placeholder: string;\n /** Accessible name of the slot's field. Defaults to `placeholder`. */\n label?: string;\n /** Type-ahead options offered while typing (the field stays free text). */\n options?: string[];\n /** Must be filled before the template can be used. */\n required?: boolean;\n}\n\n/** A literal string, or a fill-in-the-blank slot. */\nexport type PromptSegment = string | PromptSlot;\n\nexport interface TemplatesProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n /** The template as an ordered mix of literal text and named slots. */\n segments: PromptSegment[];\n /** Slot values (controlled), keyed by slot id. */\n values?: Record<string, string>;\n /** Initial slot values (uncontrolled). */\n defaultValues?: Record<string, string>;\n /** Called when any slot changes. */\n onValuesChange?: (values: Record<string, string>) => void;\n /** Called with the interpolated prompt (and raw values) on submit. Nothing runs on its own. */\n onComplete?: (prompt: string, values: Record<string, string>) => void;\n /** Submit button label. Defaults to `\"Use this prompt\"`. */\n submitLabel?: React.ReactNode;\n}\n\n/**\n * The fill-in-the-blank template: a sentence with highlighted slots the user\n * tabs through, so a good prompt reads as a sentence even before it is\n * filled. Slots are free-text fields with optional type-ahead `options`;\n * `required` slots are counted (\"1 blank left\") and must be filled before the\n * one-click \"Use this prompt\" fires `onComplete` with the interpolated text.\n * For a list of fully written prompts use {@link TemplatesList}; for a library\n * grouped by domain use {@link TemplatesBrowser}.\n */\nexport const Templates = React.forwardRef<HTMLFormElement, TemplatesProps>(\n (\n {\n segments,\n values,\n defaultValues = {},\n onValuesChange,\n onComplete,\n submitLabel = \"Use this prompt\",\n className,\n ...props\n },\n ref,\n ) => {\n const [internal, setInternal] = React.useState<Record<string, string>>(defaultValues);\n const current = values ?? internal;\n const baseId = React.useId();\n\n const setSlot = (slot: string, val: string) => {\n const next = { ...current, [slot]: val };\n if (values === undefined) setInternal(next);\n onValuesChange?.(next);\n };\n\n const interpolate = () =>\n segments\n .map((s) => (typeof s === \"string\" ? s : current[s.slot] || s.placeholder))\n .join(\"\");\n\n const required = segments.filter((s): s is PromptSlot => typeof s !== \"string\" && !!s.required);\n const missing = required.filter((s) => !(current[s.slot] ?? \"\").trim()).length;\n const statusId = `${baseId}-status`;\n\n return (\n <form\n ref={ref}\n onSubmit={(e) => {\n e.preventDefault();\n if (missing > 0) return;\n onComplete?.(interpolate(), current);\n }}\n className={cn(\"flex flex-col gap-4 rounded-xl border border-border bg-background p-5\", className)}\n {...props}\n >\n <p className=\"text-base leading-loose text-foreground\">\n {segments.map((segment, i) => {\n if (typeof segment === \"string\") return <React.Fragment key={i}>{segment}</React.Fragment>;\n const val = current[segment.slot] ?? \"\";\n const listId = segment.options?.length ? `${baseId}-${segment.slot}-options` : undefined;\n return (\n <React.Fragment key={i}>\n <input\n type=\"text\"\n value={val}\n onChange={(e) => setSlot(segment.slot, e.target.value)}\n placeholder={segment.placeholder}\n aria-label={segment.label ?? segment.placeholder}\n aria-required={segment.required || undefined}\n list={listId}\n autoComplete=\"off\"\n size={Math.max((val || segment.placeholder).length, 4)}\n className={cn(\n \"mx-0.5 inline-block rounded-md border-b-2 border-dashed border-brand/50 bg-brand-muted/40 px-1 text-center text-brand placeholder:text-brand/60 focus:border-solid focus:border-brand focus:outline-none\",\n val && \"border-solid bg-brand-muted\",\n )}\n />\n {listId && (\n <datalist id={listId}>\n {segment.options!.map((o) => (\n <option key={o} value={o} />\n ))}\n </datalist>\n )}\n </React.Fragment>\n );\n })}\n </p>\n <div className=\"flex items-center justify-end gap-3\">\n {required.length > 0 && (\n <span id={statusId} aria-live=\"polite\" className=\"mr-auto text-sm text-muted-foreground\">\n {missing === 0 ? \"Ready to use\" : `${missing} ${missing === 1 ? \"blank\" : \"blanks\"} left`}\n </span>\n )}\n <Button\n type=\"submit\"\n variant=\"cta\"\n disabled={missing > 0}\n aria-describedby={required.length > 0 ? statusId : undefined}\n >\n {submitLabel}\n </Button>\n </div>\n </form>\n );\n },\n);\nTemplates.displayName = \"Templates\";\n\n/** A fully written template in a list or browser. */\nexport interface TemplatesListItem {\n /** Stable id. Defaults to `title`. */\n id?: string;\n /** What the user sees (\"Summarise yesterday's standup in 3 bullets\"). */\n title: string;\n /** The prompt handed to `onSelect`. Defaults to `title`. */\n prompt?: string;\n /** One supporting line. */\n description?: string;\n}\n\nexport interface TemplatesListProps extends Omit<React.HTMLAttributes<HTMLUListElement>, \"onSelect\"> {\n /** The templates, as strings or {@link TemplatesListItem}s. */\n items: Array<string | TemplatesListItem>;\n /** Called with the prompt and the item when one is picked. Place it in the composer — never send it. */\n onSelect?: (prompt: string, item: TemplatesListItem) => void;\n}\n\nconst toListItem = (item: string | TemplatesListItem): TemplatesListItem =>\n typeof item === \"string\" ? { title: item } : item;\n\n/**\n * The prefilled-list template surface: fully written prompts the user runs\n * with one tap, each a keyboard-reachable button in a labelled list. The\n * lowest-friction template surface; pass `aria-label` to name the list.\n */\nexport const TemplatesList = React.forwardRef<HTMLUListElement, TemplatesListProps>(\n ({ items, onSelect, className, ...props }, ref) => (\n <ul\n ref={ref}\n role=\"list\"\n className={cn(\"m-0 flex list-none flex-col gap-1 p-0\", className)}\n {...props}\n >\n {items.map(toListItem).map((item) => (\n <li key={item.id ?? item.title}>\n <button\n type=\"button\"\n onClick={() => onSelect?.(item.prompt ?? item.title, item)}\n className=\"flex w-full items-start gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <ArrowRight aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-muted-foreground\" />\n <span className=\"flex min-w-0 flex-col gap-0.5\">\n <span>{item.title}</span>\n {item.description && <span className=\"text-xs text-muted-foreground\">{item.description}</span>}\n </span>\n </button>\n </li>\n ))}\n </ul>\n ),\n);\nTemplatesList.displayName = \"TemplatesList\";\n\n/** A named group of templates in a {@link TemplatesBrowser}. */\nexport interface TemplatesCategory {\n /** Category id. */\n id: string;\n /** Tab label (\"Sales\"). */\n label: string;\n /** The templates in this category. */\n templates: Array<string | TemplatesListItem>;\n}\n\nexport interface TemplatesBrowserProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\" | \"defaultValue\"> {\n /** The categories, in display order. */\n categories: TemplatesCategory[];\n /** Controlled active category id. */\n category?: string;\n /** Initial category when uncontrolled. Defaults to the first. */\n defaultCategory?: string;\n /** Called when the active category changes. */\n onCategoryChange?: (id: string) => void;\n /** Called with the prompt, the item and the category id when a template is picked. */\n onSelect?: (prompt: string, item: TemplatesListItem, category: string) => void;\n}\n\n/**\n * The categorised template browser: a library of templates grouped by domain,\n * with a vertical tab per category (arrow keys move between them) and the\n * category's templates as a {@link TemplatesList}. Scales to many prompt\n * families.\n */\nexport const TemplatesBrowser = React.forwardRef<HTMLDivElement, TemplatesBrowserProps>(\n ({ categories, category, defaultCategory, onCategoryChange, onSelect, className, ...props }, ref) => (\n <Tabs\n ref={ref}\n orientation=\"vertical\"\n value={category}\n defaultValue={defaultCategory ?? categories[0]?.id ?? \"\"}\n onValueChange={onCategoryChange}\n className={cn(\"gap-3\", className)}\n {...props}\n >\n <TabsList aria-label=\"Template categories\" className=\"h-fit shrink-0 bg-transparent p-0\">\n {categories.map((c) => (\n <TabsTrigger key={c.id} value={c.id} className=\"justify-start aria-selected:bg-muted aria-selected:shadow-none\">\n {c.label}\n </TabsTrigger>\n ))}\n </TabsList>\n {categories.map((c) => (\n <TabsContent key={c.id} value={c.id} className=\"min-w-0 flex-1\">\n <TemplatesList\n aria-label={`${c.label} templates`}\n items={c.templates}\n onSelect={(prompt, item) => onSelect?.(prompt, item, c.id)}\n />\n </TabsContent>\n ))}\n </Tabs>\n ),\n);\nTemplatesBrowser.displayName = \"TemplatesBrowser\";\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** A fill-in-the-blank slot: an id, the placeholder shown while empty, and optional type-ahead options. */
|
|
4
|
+
interface PromptSlot {
|
|
5
|
+
/** Slot id — the key in `values`. */
|
|
6
|
+
slot: string;
|
|
7
|
+
/** Shown while the slot is empty, and used in the prompt if it stays empty. */
|
|
8
|
+
placeholder: string;
|
|
9
|
+
/** Accessible name of the slot's field. Defaults to `placeholder`. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Type-ahead options offered while typing (the field stays free text). */
|
|
12
|
+
options?: string[];
|
|
13
|
+
/** Must be filled before the template can be used. */
|
|
14
|
+
required?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** A literal string, or a fill-in-the-blank slot. */
|
|
17
|
+
type PromptSegment = string | PromptSlot;
|
|
18
|
+
interface TemplatesProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "onSubmit"> {
|
|
19
|
+
/** The template as an ordered mix of literal text and named slots. */
|
|
20
|
+
segments: PromptSegment[];
|
|
21
|
+
/** Slot values (controlled), keyed by slot id. */
|
|
22
|
+
values?: Record<string, string>;
|
|
23
|
+
/** Initial slot values (uncontrolled). */
|
|
24
|
+
defaultValues?: Record<string, string>;
|
|
25
|
+
/** Called when any slot changes. */
|
|
26
|
+
onValuesChange?: (values: Record<string, string>) => void;
|
|
27
|
+
/** Called with the interpolated prompt (and raw values) on submit. Nothing runs on its own. */
|
|
28
|
+
onComplete?: (prompt: string, values: Record<string, string>) => void;
|
|
29
|
+
/** Submit button label. Defaults to `"Use this prompt"`. */
|
|
30
|
+
submitLabel?: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The fill-in-the-blank template: a sentence with highlighted slots the user
|
|
34
|
+
* tabs through, so a good prompt reads as a sentence even before it is
|
|
35
|
+
* filled. Slots are free-text fields with optional type-ahead `options`;
|
|
36
|
+
* `required` slots are counted ("1 blank left") and must be filled before the
|
|
37
|
+
* one-click "Use this prompt" fires `onComplete` with the interpolated text.
|
|
38
|
+
* For a list of fully written prompts use {@link TemplatesList}; for a library
|
|
39
|
+
* grouped by domain use {@link TemplatesBrowser}.
|
|
40
|
+
*/
|
|
41
|
+
declare const Templates: React.ForwardRefExoticComponent<TemplatesProps & React.RefAttributes<HTMLFormElement>>;
|
|
42
|
+
/** A fully written template in a list or browser. */
|
|
43
|
+
interface TemplatesListItem {
|
|
44
|
+
/** Stable id. Defaults to `title`. */
|
|
45
|
+
id?: string;
|
|
46
|
+
/** What the user sees ("Summarise yesterday's standup in 3 bullets"). */
|
|
47
|
+
title: string;
|
|
48
|
+
/** The prompt handed to `onSelect`. Defaults to `title`. */
|
|
49
|
+
prompt?: string;
|
|
50
|
+
/** One supporting line. */
|
|
51
|
+
description?: string;
|
|
52
|
+
}
|
|
53
|
+
interface TemplatesListProps extends Omit<React.HTMLAttributes<HTMLUListElement>, "onSelect"> {
|
|
54
|
+
/** The templates, as strings or {@link TemplatesListItem}s. */
|
|
55
|
+
items: Array<string | TemplatesListItem>;
|
|
56
|
+
/** Called with the prompt and the item when one is picked. Place it in the composer — never send it. */
|
|
57
|
+
onSelect?: (prompt: string, item: TemplatesListItem) => void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The prefilled-list template surface: fully written prompts the user runs
|
|
61
|
+
* with one tap, each a keyboard-reachable button in a labelled list. The
|
|
62
|
+
* lowest-friction template surface; pass `aria-label` to name the list.
|
|
63
|
+
*/
|
|
64
|
+
declare const TemplatesList: React.ForwardRefExoticComponent<TemplatesListProps & React.RefAttributes<HTMLUListElement>>;
|
|
65
|
+
/** A named group of templates in a {@link TemplatesBrowser}. */
|
|
66
|
+
interface TemplatesCategory {
|
|
67
|
+
/** Category id. */
|
|
68
|
+
id: string;
|
|
69
|
+
/** Tab label ("Sales"). */
|
|
70
|
+
label: string;
|
|
71
|
+
/** The templates in this category. */
|
|
72
|
+
templates: Array<string | TemplatesListItem>;
|
|
73
|
+
}
|
|
74
|
+
interface TemplatesBrowserProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect" | "defaultValue"> {
|
|
75
|
+
/** The categories, in display order. */
|
|
76
|
+
categories: TemplatesCategory[];
|
|
77
|
+
/** Controlled active category id. */
|
|
78
|
+
category?: string;
|
|
79
|
+
/** Initial category when uncontrolled. Defaults to the first. */
|
|
80
|
+
defaultCategory?: string;
|
|
81
|
+
/** Called when the active category changes. */
|
|
82
|
+
onCategoryChange?: (id: string) => void;
|
|
83
|
+
/** Called with the prompt, the item and the category id when a template is picked. */
|
|
84
|
+
onSelect?: (prompt: string, item: TemplatesListItem, category: string) => void;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The categorised template browser: a library of templates grouped by domain,
|
|
88
|
+
* with a vertical tab per category (arrow keys move between them) and the
|
|
89
|
+
* category's templates as a {@link TemplatesList}. Scales to many prompt
|
|
90
|
+
* families.
|
|
91
|
+
*/
|
|
92
|
+
declare const TemplatesBrowser: React.ForwardRefExoticComponent<TemplatesBrowserProps & React.RefAttributes<HTMLDivElement>>;
|
|
93
|
+
|
|
94
|
+
export { type PromptSegment, type PromptSlot, Templates, TemplatesBrowser, type TemplatesBrowserProps, type TemplatesCategory, TemplatesList, type TemplatesListItem, type TemplatesListProps, type TemplatesProps };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
/** A fill-in-the-blank slot: an id, the placeholder shown while empty, and optional type-ahead options. */
|
|
4
|
+
interface PromptSlot {
|
|
5
|
+
/** Slot id — the key in `values`. */
|
|
6
|
+
slot: string;
|
|
7
|
+
/** Shown while the slot is empty, and used in the prompt if it stays empty. */
|
|
8
|
+
placeholder: string;
|
|
9
|
+
/** Accessible name of the slot's field. Defaults to `placeholder`. */
|
|
10
|
+
label?: string;
|
|
11
|
+
/** Type-ahead options offered while typing (the field stays free text). */
|
|
12
|
+
options?: string[];
|
|
13
|
+
/** Must be filled before the template can be used. */
|
|
14
|
+
required?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** A literal string, or a fill-in-the-blank slot. */
|
|
17
|
+
type PromptSegment = string | PromptSlot;
|
|
18
|
+
interface TemplatesProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, "onSubmit"> {
|
|
19
|
+
/** The template as an ordered mix of literal text and named slots. */
|
|
20
|
+
segments: PromptSegment[];
|
|
21
|
+
/** Slot values (controlled), keyed by slot id. */
|
|
22
|
+
values?: Record<string, string>;
|
|
23
|
+
/** Initial slot values (uncontrolled). */
|
|
24
|
+
defaultValues?: Record<string, string>;
|
|
25
|
+
/** Called when any slot changes. */
|
|
26
|
+
onValuesChange?: (values: Record<string, string>) => void;
|
|
27
|
+
/** Called with the interpolated prompt (and raw values) on submit. Nothing runs on its own. */
|
|
28
|
+
onComplete?: (prompt: string, values: Record<string, string>) => void;
|
|
29
|
+
/** Submit button label. Defaults to `"Use this prompt"`. */
|
|
30
|
+
submitLabel?: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The fill-in-the-blank template: a sentence with highlighted slots the user
|
|
34
|
+
* tabs through, so a good prompt reads as a sentence even before it is
|
|
35
|
+
* filled. Slots are free-text fields with optional type-ahead `options`;
|
|
36
|
+
* `required` slots are counted ("1 blank left") and must be filled before the
|
|
37
|
+
* one-click "Use this prompt" fires `onComplete` with the interpolated text.
|
|
38
|
+
* For a list of fully written prompts use {@link TemplatesList}; for a library
|
|
39
|
+
* grouped by domain use {@link TemplatesBrowser}.
|
|
40
|
+
*/
|
|
41
|
+
declare const Templates: React.ForwardRefExoticComponent<TemplatesProps & React.RefAttributes<HTMLFormElement>>;
|
|
42
|
+
/** A fully written template in a list or browser. */
|
|
43
|
+
interface TemplatesListItem {
|
|
44
|
+
/** Stable id. Defaults to `title`. */
|
|
45
|
+
id?: string;
|
|
46
|
+
/** What the user sees ("Summarise yesterday's standup in 3 bullets"). */
|
|
47
|
+
title: string;
|
|
48
|
+
/** The prompt handed to `onSelect`. Defaults to `title`. */
|
|
49
|
+
prompt?: string;
|
|
50
|
+
/** One supporting line. */
|
|
51
|
+
description?: string;
|
|
52
|
+
}
|
|
53
|
+
interface TemplatesListProps extends Omit<React.HTMLAttributes<HTMLUListElement>, "onSelect"> {
|
|
54
|
+
/** The templates, as strings or {@link TemplatesListItem}s. */
|
|
55
|
+
items: Array<string | TemplatesListItem>;
|
|
56
|
+
/** Called with the prompt and the item when one is picked. Place it in the composer — never send it. */
|
|
57
|
+
onSelect?: (prompt: string, item: TemplatesListItem) => void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The prefilled-list template surface: fully written prompts the user runs
|
|
61
|
+
* with one tap, each a keyboard-reachable button in a labelled list. The
|
|
62
|
+
* lowest-friction template surface; pass `aria-label` to name the list.
|
|
63
|
+
*/
|
|
64
|
+
declare const TemplatesList: React.ForwardRefExoticComponent<TemplatesListProps & React.RefAttributes<HTMLUListElement>>;
|
|
65
|
+
/** A named group of templates in a {@link TemplatesBrowser}. */
|
|
66
|
+
interface TemplatesCategory {
|
|
67
|
+
/** Category id. */
|
|
68
|
+
id: string;
|
|
69
|
+
/** Tab label ("Sales"). */
|
|
70
|
+
label: string;
|
|
71
|
+
/** The templates in this category. */
|
|
72
|
+
templates: Array<string | TemplatesListItem>;
|
|
73
|
+
}
|
|
74
|
+
interface TemplatesBrowserProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onSelect" | "defaultValue"> {
|
|
75
|
+
/** The categories, in display order. */
|
|
76
|
+
categories: TemplatesCategory[];
|
|
77
|
+
/** Controlled active category id. */
|
|
78
|
+
category?: string;
|
|
79
|
+
/** Initial category when uncontrolled. Defaults to the first. */
|
|
80
|
+
defaultCategory?: string;
|
|
81
|
+
/** Called when the active category changes. */
|
|
82
|
+
onCategoryChange?: (id: string) => void;
|
|
83
|
+
/** Called with the prompt, the item and the category id when a template is picked. */
|
|
84
|
+
onSelect?: (prompt: string, item: TemplatesListItem, category: string) => void;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The categorised template browser: a library of templates grouped by domain,
|
|
88
|
+
* with a vertical tab per category (arrow keys move between them) and the
|
|
89
|
+
* category's templates as a {@link TemplatesList}. Scales to many prompt
|
|
90
|
+
* families.
|
|
91
|
+
*/
|
|
92
|
+
declare const TemplatesBrowser: React.ForwardRefExoticComponent<TemplatesBrowserProps & React.RefAttributes<HTMLDivElement>>;
|
|
93
|
+
|
|
94
|
+
export { type PromptSegment, type PromptSlot, Templates, TemplatesBrowser, type TemplatesBrowserProps, type TemplatesCategory, TemplatesList, type TemplatesListItem, type TemplatesListProps, type TemplatesProps };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { ArrowRight } from 'lucide-react';
|
|
4
|
+
import { cn } from '@nucleux/utils';
|
|
5
|
+
import { Button } from '@nucleux/button';
|
|
6
|
+
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@nucleux/tabs';
|
|
7
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
8
|
+
|
|
9
|
+
// src/index.tsx
|
|
10
|
+
var Templates = React.forwardRef(
|
|
11
|
+
({
|
|
12
|
+
segments,
|
|
13
|
+
values,
|
|
14
|
+
defaultValues = {},
|
|
15
|
+
onValuesChange,
|
|
16
|
+
onComplete,
|
|
17
|
+
submitLabel = "Use this prompt",
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}, ref) => {
|
|
21
|
+
const [internal, setInternal] = React.useState(defaultValues);
|
|
22
|
+
const current = values ?? internal;
|
|
23
|
+
const baseId = React.useId();
|
|
24
|
+
const setSlot = (slot, val) => {
|
|
25
|
+
const next = { ...current, [slot]: val };
|
|
26
|
+
if (values === void 0) setInternal(next);
|
|
27
|
+
onValuesChange?.(next);
|
|
28
|
+
};
|
|
29
|
+
const interpolate = () => segments.map((s) => typeof s === "string" ? s : current[s.slot] || s.placeholder).join("");
|
|
30
|
+
const required = segments.filter((s) => typeof s !== "string" && !!s.required);
|
|
31
|
+
const missing = required.filter((s) => !(current[s.slot] ?? "").trim()).length;
|
|
32
|
+
const statusId = `${baseId}-status`;
|
|
33
|
+
return /* @__PURE__ */ jsxs(
|
|
34
|
+
"form",
|
|
35
|
+
{
|
|
36
|
+
ref,
|
|
37
|
+
onSubmit: (e) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
if (missing > 0) return;
|
|
40
|
+
onComplete?.(interpolate(), current);
|
|
41
|
+
},
|
|
42
|
+
className: cn("flex flex-col gap-4 rounded-xl border border-border bg-background p-5", className),
|
|
43
|
+
...props,
|
|
44
|
+
children: [
|
|
45
|
+
/* @__PURE__ */ jsx("p", { className: "text-base leading-loose text-foreground", children: segments.map((segment, i) => {
|
|
46
|
+
if (typeof segment === "string") return /* @__PURE__ */ jsx(React.Fragment, { children: segment }, i);
|
|
47
|
+
const val = current[segment.slot] ?? "";
|
|
48
|
+
const listId = segment.options?.length ? `${baseId}-${segment.slot}-options` : void 0;
|
|
49
|
+
return /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
50
|
+
/* @__PURE__ */ jsx(
|
|
51
|
+
"input",
|
|
52
|
+
{
|
|
53
|
+
type: "text",
|
|
54
|
+
value: val,
|
|
55
|
+
onChange: (e) => setSlot(segment.slot, e.target.value),
|
|
56
|
+
placeholder: segment.placeholder,
|
|
57
|
+
"aria-label": segment.label ?? segment.placeholder,
|
|
58
|
+
"aria-required": segment.required || void 0,
|
|
59
|
+
list: listId,
|
|
60
|
+
autoComplete: "off",
|
|
61
|
+
size: Math.max((val || segment.placeholder).length, 4),
|
|
62
|
+
className: cn(
|
|
63
|
+
"mx-0.5 inline-block rounded-md border-b-2 border-dashed border-brand/50 bg-brand-muted/40 px-1 text-center text-brand placeholder:text-brand/60 focus:border-solid focus:border-brand focus:outline-none",
|
|
64
|
+
val && "border-solid bg-brand-muted"
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
),
|
|
68
|
+
listId && /* @__PURE__ */ jsx("datalist", { id: listId, children: segment.options.map((o) => /* @__PURE__ */ jsx("option", { value: o }, o)) })
|
|
69
|
+
] }, i);
|
|
70
|
+
}) }),
|
|
71
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-3", children: [
|
|
72
|
+
required.length > 0 && /* @__PURE__ */ jsx("span", { id: statusId, "aria-live": "polite", className: "mr-auto text-sm text-muted-foreground", children: missing === 0 ? "Ready to use" : `${missing} ${missing === 1 ? "blank" : "blanks"} left` }),
|
|
73
|
+
/* @__PURE__ */ jsx(
|
|
74
|
+
Button,
|
|
75
|
+
{
|
|
76
|
+
type: "submit",
|
|
77
|
+
variant: "cta",
|
|
78
|
+
disabled: missing > 0,
|
|
79
|
+
"aria-describedby": required.length > 0 ? statusId : void 0,
|
|
80
|
+
children: submitLabel
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
] })
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
Templates.displayName = "Templates";
|
|
90
|
+
var toListItem = (item) => typeof item === "string" ? { title: item } : item;
|
|
91
|
+
var TemplatesList = React.forwardRef(
|
|
92
|
+
({ items, onSelect, className, ...props }, ref) => /* @__PURE__ */ jsx(
|
|
93
|
+
"ul",
|
|
94
|
+
{
|
|
95
|
+
ref,
|
|
96
|
+
role: "list",
|
|
97
|
+
className: cn("m-0 flex list-none flex-col gap-1 p-0", className),
|
|
98
|
+
...props,
|
|
99
|
+
children: items.map(toListItem).map((item) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
100
|
+
"button",
|
|
101
|
+
{
|
|
102
|
+
type: "button",
|
|
103
|
+
onClick: () => onSelect?.(item.prompt ?? item.title, item),
|
|
104
|
+
className: "flex w-full items-start gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
105
|
+
children: [
|
|
106
|
+
/* @__PURE__ */ jsx(ArrowRight, { "aria-hidden": "true", className: "mt-0.5 size-4 shrink-0 text-muted-foreground" }),
|
|
107
|
+
/* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [
|
|
108
|
+
/* @__PURE__ */ jsx("span", { children: item.title }),
|
|
109
|
+
item.description && /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: item.description })
|
|
110
|
+
] })
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
) }, item.id ?? item.title))
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
TemplatesList.displayName = "TemplatesList";
|
|
118
|
+
var TemplatesBrowser = React.forwardRef(
|
|
119
|
+
({ categories, category, defaultCategory, onCategoryChange, onSelect, className, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
120
|
+
Tabs,
|
|
121
|
+
{
|
|
122
|
+
ref,
|
|
123
|
+
orientation: "vertical",
|
|
124
|
+
value: category,
|
|
125
|
+
defaultValue: defaultCategory ?? categories[0]?.id ?? "",
|
|
126
|
+
onValueChange: onCategoryChange,
|
|
127
|
+
className: cn("gap-3", className),
|
|
128
|
+
...props,
|
|
129
|
+
children: [
|
|
130
|
+
/* @__PURE__ */ jsx(TabsList, { "aria-label": "Template categories", className: "h-fit shrink-0 bg-transparent p-0", children: categories.map((c) => /* @__PURE__ */ jsx(TabsTrigger, { value: c.id, className: "justify-start aria-selected:bg-muted aria-selected:shadow-none", children: c.label }, c.id)) }),
|
|
131
|
+
categories.map((c) => /* @__PURE__ */ jsx(TabsContent, { value: c.id, className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(
|
|
132
|
+
TemplatesList,
|
|
133
|
+
{
|
|
134
|
+
"aria-label": `${c.label} templates`,
|
|
135
|
+
items: c.templates,
|
|
136
|
+
onSelect: (prompt, item) => onSelect?.(prompt, item, c.id)
|
|
137
|
+
}
|
|
138
|
+
) }, c.id))
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
TemplatesBrowser.displayName = "TemplatesBrowser";
|
|
144
|
+
|
|
145
|
+
export { Templates, TemplatesBrowser, TemplatesList };
|
|
146
|
+
//# sourceMappingURL=index.js.map
|
|
147
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.tsx"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import * as React from \"react\";\nimport { ArrowRight } from \"lucide-react\";\nimport { cn } from \"@nucleux/utils\";\nimport { Button } from \"@nucleux/button\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@nucleux/tabs\";\n\n/** A fill-in-the-blank slot: an id, the placeholder shown while empty, and optional type-ahead options. */\nexport interface PromptSlot {\n /** Slot id — the key in `values`. */\n slot: string;\n /** Shown while the slot is empty, and used in the prompt if it stays empty. */\n placeholder: string;\n /** Accessible name of the slot's field. Defaults to `placeholder`. */\n label?: string;\n /** Type-ahead options offered while typing (the field stays free text). */\n options?: string[];\n /** Must be filled before the template can be used. */\n required?: boolean;\n}\n\n/** A literal string, or a fill-in-the-blank slot. */\nexport type PromptSegment = string | PromptSlot;\n\nexport interface TemplatesProps extends Omit<React.FormHTMLAttributes<HTMLFormElement>, \"onSubmit\"> {\n /** The template as an ordered mix of literal text and named slots. */\n segments: PromptSegment[];\n /** Slot values (controlled), keyed by slot id. */\n values?: Record<string, string>;\n /** Initial slot values (uncontrolled). */\n defaultValues?: Record<string, string>;\n /** Called when any slot changes. */\n onValuesChange?: (values: Record<string, string>) => void;\n /** Called with the interpolated prompt (and raw values) on submit. Nothing runs on its own. */\n onComplete?: (prompt: string, values: Record<string, string>) => void;\n /** Submit button label. Defaults to `\"Use this prompt\"`. */\n submitLabel?: React.ReactNode;\n}\n\n/**\n * The fill-in-the-blank template: a sentence with highlighted slots the user\n * tabs through, so a good prompt reads as a sentence even before it is\n * filled. Slots are free-text fields with optional type-ahead `options`;\n * `required` slots are counted (\"1 blank left\") and must be filled before the\n * one-click \"Use this prompt\" fires `onComplete` with the interpolated text.\n * For a list of fully written prompts use {@link TemplatesList}; for a library\n * grouped by domain use {@link TemplatesBrowser}.\n */\nexport const Templates = React.forwardRef<HTMLFormElement, TemplatesProps>(\n (\n {\n segments,\n values,\n defaultValues = {},\n onValuesChange,\n onComplete,\n submitLabel = \"Use this prompt\",\n className,\n ...props\n },\n ref,\n ) => {\n const [internal, setInternal] = React.useState<Record<string, string>>(defaultValues);\n const current = values ?? internal;\n const baseId = React.useId();\n\n const setSlot = (slot: string, val: string) => {\n const next = { ...current, [slot]: val };\n if (values === undefined) setInternal(next);\n onValuesChange?.(next);\n };\n\n const interpolate = () =>\n segments\n .map((s) => (typeof s === \"string\" ? s : current[s.slot] || s.placeholder))\n .join(\"\");\n\n const required = segments.filter((s): s is PromptSlot => typeof s !== \"string\" && !!s.required);\n const missing = required.filter((s) => !(current[s.slot] ?? \"\").trim()).length;\n const statusId = `${baseId}-status`;\n\n return (\n <form\n ref={ref}\n onSubmit={(e) => {\n e.preventDefault();\n if (missing > 0) return;\n onComplete?.(interpolate(), current);\n }}\n className={cn(\"flex flex-col gap-4 rounded-xl border border-border bg-background p-5\", className)}\n {...props}\n >\n <p className=\"text-base leading-loose text-foreground\">\n {segments.map((segment, i) => {\n if (typeof segment === \"string\") return <React.Fragment key={i}>{segment}</React.Fragment>;\n const val = current[segment.slot] ?? \"\";\n const listId = segment.options?.length ? `${baseId}-${segment.slot}-options` : undefined;\n return (\n <React.Fragment key={i}>\n <input\n type=\"text\"\n value={val}\n onChange={(e) => setSlot(segment.slot, e.target.value)}\n placeholder={segment.placeholder}\n aria-label={segment.label ?? segment.placeholder}\n aria-required={segment.required || undefined}\n list={listId}\n autoComplete=\"off\"\n size={Math.max((val || segment.placeholder).length, 4)}\n className={cn(\n \"mx-0.5 inline-block rounded-md border-b-2 border-dashed border-brand/50 bg-brand-muted/40 px-1 text-center text-brand placeholder:text-brand/60 focus:border-solid focus:border-brand focus:outline-none\",\n val && \"border-solid bg-brand-muted\",\n )}\n />\n {listId && (\n <datalist id={listId}>\n {segment.options!.map((o) => (\n <option key={o} value={o} />\n ))}\n </datalist>\n )}\n </React.Fragment>\n );\n })}\n </p>\n <div className=\"flex items-center justify-end gap-3\">\n {required.length > 0 && (\n <span id={statusId} aria-live=\"polite\" className=\"mr-auto text-sm text-muted-foreground\">\n {missing === 0 ? \"Ready to use\" : `${missing} ${missing === 1 ? \"blank\" : \"blanks\"} left`}\n </span>\n )}\n <Button\n type=\"submit\"\n variant=\"cta\"\n disabled={missing > 0}\n aria-describedby={required.length > 0 ? statusId : undefined}\n >\n {submitLabel}\n </Button>\n </div>\n </form>\n );\n },\n);\nTemplates.displayName = \"Templates\";\n\n/** A fully written template in a list or browser. */\nexport interface TemplatesListItem {\n /** Stable id. Defaults to `title`. */\n id?: string;\n /** What the user sees (\"Summarise yesterday's standup in 3 bullets\"). */\n title: string;\n /** The prompt handed to `onSelect`. Defaults to `title`. */\n prompt?: string;\n /** One supporting line. */\n description?: string;\n}\n\nexport interface TemplatesListProps extends Omit<React.HTMLAttributes<HTMLUListElement>, \"onSelect\"> {\n /** The templates, as strings or {@link TemplatesListItem}s. */\n items: Array<string | TemplatesListItem>;\n /** Called with the prompt and the item when one is picked. Place it in the composer — never send it. */\n onSelect?: (prompt: string, item: TemplatesListItem) => void;\n}\n\nconst toListItem = (item: string | TemplatesListItem): TemplatesListItem =>\n typeof item === \"string\" ? { title: item } : item;\n\n/**\n * The prefilled-list template surface: fully written prompts the user runs\n * with one tap, each a keyboard-reachable button in a labelled list. The\n * lowest-friction template surface; pass `aria-label` to name the list.\n */\nexport const TemplatesList = React.forwardRef<HTMLUListElement, TemplatesListProps>(\n ({ items, onSelect, className, ...props }, ref) => (\n <ul\n ref={ref}\n role=\"list\"\n className={cn(\"m-0 flex list-none flex-col gap-1 p-0\", className)}\n {...props}\n >\n {items.map(toListItem).map((item) => (\n <li key={item.id ?? item.title}>\n <button\n type=\"button\"\n onClick={() => onSelect?.(item.prompt ?? item.title, item)}\n className=\"flex w-full items-start gap-2 rounded-md border border-border bg-muted/40 px-3 py-2 text-left text-sm text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n >\n <ArrowRight aria-hidden=\"true\" className=\"mt-0.5 size-4 shrink-0 text-muted-foreground\" />\n <span className=\"flex min-w-0 flex-col gap-0.5\">\n <span>{item.title}</span>\n {item.description && <span className=\"text-xs text-muted-foreground\">{item.description}</span>}\n </span>\n </button>\n </li>\n ))}\n </ul>\n ),\n);\nTemplatesList.displayName = \"TemplatesList\";\n\n/** A named group of templates in a {@link TemplatesBrowser}. */\nexport interface TemplatesCategory {\n /** Category id. */\n id: string;\n /** Tab label (\"Sales\"). */\n label: string;\n /** The templates in this category. */\n templates: Array<string | TemplatesListItem>;\n}\n\nexport interface TemplatesBrowserProps\n extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\" | \"defaultValue\"> {\n /** The categories, in display order. */\n categories: TemplatesCategory[];\n /** Controlled active category id. */\n category?: string;\n /** Initial category when uncontrolled. Defaults to the first. */\n defaultCategory?: string;\n /** Called when the active category changes. */\n onCategoryChange?: (id: string) => void;\n /** Called with the prompt, the item and the category id when a template is picked. */\n onSelect?: (prompt: string, item: TemplatesListItem, category: string) => void;\n}\n\n/**\n * The categorised template browser: a library of templates grouped by domain,\n * with a vertical tab per category (arrow keys move between them) and the\n * category's templates as a {@link TemplatesList}. Scales to many prompt\n * families.\n */\nexport const TemplatesBrowser = React.forwardRef<HTMLDivElement, TemplatesBrowserProps>(\n ({ categories, category, defaultCategory, onCategoryChange, onSelect, className, ...props }, ref) => (\n <Tabs\n ref={ref}\n orientation=\"vertical\"\n value={category}\n defaultValue={defaultCategory ?? categories[0]?.id ?? \"\"}\n onValueChange={onCategoryChange}\n className={cn(\"gap-3\", className)}\n {...props}\n >\n <TabsList aria-label=\"Template categories\" className=\"h-fit shrink-0 bg-transparent p-0\">\n {categories.map((c) => (\n <TabsTrigger key={c.id} value={c.id} className=\"justify-start aria-selected:bg-muted aria-selected:shadow-none\">\n {c.label}\n </TabsTrigger>\n ))}\n </TabsList>\n {categories.map((c) => (\n <TabsContent key={c.id} value={c.id} className=\"min-w-0 flex-1\">\n <TemplatesList\n aria-label={`${c.label} templates`}\n items={c.templates}\n onSelect={(prompt, item) => onSelect?.(prompt, item, c.id)}\n />\n </TabsContent>\n ))}\n </Tabs>\n ),\n);\nTemplatesBrowser.displayName = \"TemplatesBrowser\";\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nucleux/templates",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"description": "Templates — fill-in-the-blank prompt scaffolds that turn vague intent into a good question — Nucleux agentic UI for React.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"nucleux",
|
|
9
|
+
"react",
|
|
10
|
+
"ui",
|
|
11
|
+
"components",
|
|
12
|
+
"agentic",
|
|
13
|
+
"ai",
|
|
14
|
+
"tailwind",
|
|
15
|
+
"templates"
|
|
16
|
+
],
|
|
17
|
+
"main": "./dist/index.cjs",
|
|
18
|
+
"module": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"require": "./dist/index.cjs"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"react": ">=18",
|
|
33
|
+
"react-dom": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@nucleux/button": "0.7.0",
|
|
37
|
+
"@nucleux/tabs": "0.7.0",
|
|
38
|
+
"@nucleux/utils": "0.7.0",
|
|
39
|
+
"lucide-react": "^0.469.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"tsup": "^8.3.5",
|
|
43
|
+
"typescript": "^5.7.2",
|
|
44
|
+
"@types/react": "^18.3.18",
|
|
45
|
+
"react": "^18.3.1"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/intelligaia-open-labs/nucleux.git",
|
|
54
|
+
"directory": "packages/templates"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/intelligaia-open-labs/nucleux#readme",
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup"
|
|
59
|
+
}
|
|
6
60
|
}
|