@joplo-app/plugin-ui 0.0.0-stage → 0.3.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.
@@ -0,0 +1,129 @@
1
+ import { component } from '@joplo-app/kit/node';
2
+ const c = (name) => component(`ui:${name}`, '@joplo-app/plugin-ui');
3
+ // ---------- 布局 ----------
4
+ export const AppShell = Object.assign(c('AppShell'), {
5
+ Header: c('AppShell.Header'),
6
+ Navbar: c('AppShell.Navbar'),
7
+ Main: c('AppShell.Main'),
8
+ Aside: c('AppShell.Aside'),
9
+ Footer: c('AppShell.Footer'),
10
+ });
11
+ export const Container = c('Container');
12
+ export const Stack = c('Stack');
13
+ export const Group = c('Group');
14
+ export const Flex = c('Flex');
15
+ export const Grid = Object.assign(c('Grid'), { Col: c('Grid.Col') });
16
+ export const SimpleGrid = c('SimpleGrid');
17
+ export const Center = c('Center');
18
+ export const Box = c('Box');
19
+ export const Space = c('Space');
20
+ export const Paper = c('Paper');
21
+ export const Card = Object.assign(c('Card'), { Section: c('Card.Section') });
22
+ export const Divider = c('Divider');
23
+ export const ScrollArea = c('ScrollArea');
24
+ // ---------- 文字与展示 ----------
25
+ export const Title = c('Title');
26
+ export const Text = c('Text');
27
+ export const Code = c('Code');
28
+ export const Kbd = c('Kbd');
29
+ export const Blockquote = c('Blockquote');
30
+ export const List = Object.assign(c('List'), { Item: c('List.Item') });
31
+ export const Badge = c('Badge');
32
+ export const Avatar = Object.assign(c('Avatar'), { Group: c('Avatar.Group') });
33
+ export const Image = c('Image');
34
+ export const ThemeIcon = c('ThemeIcon');
35
+ export const Indicator = c('Indicator');
36
+ export const Timeline = Object.assign(c('Timeline'), { Item: c('Timeline.Item') });
37
+ export const Table = Object.assign(c('Table'), {
38
+ Thead: c('Table.Thead'),
39
+ Tbody: c('Table.Tbody'),
40
+ Tr: c('Table.Tr'),
41
+ Th: c('Table.Th'),
42
+ Td: c('Table.Td'),
43
+ });
44
+ export const DataTable = c('DataTable');
45
+ export const Accordion = Object.assign(c('Accordion'), {
46
+ Item: c('Accordion.Item'),
47
+ Control: c('Accordion.Control'),
48
+ Panel: c('Accordion.Panel'),
49
+ });
50
+ // ---------- 输入 ----------
51
+ export const Button = c('Button');
52
+ export const ActionIcon = c('ActionIcon');
53
+ export const CloseButton = c('CloseButton');
54
+ export const TextInput = c('TextInput');
55
+ export const PasswordInput = c('PasswordInput');
56
+ export const NumberInput = c('NumberInput');
57
+ export const Textarea = c('Textarea');
58
+ export const Select = c('Select');
59
+ export const MultiSelect = c('MultiSelect');
60
+ export const Checkbox = c('Checkbox');
61
+ export const Radio = Object.assign(c('Radio'), { Group: c('Radio.Group'), Card: c('Radio.Card') });
62
+ export const Switch = c('Switch');
63
+ export const SegmentedControl = c('SegmentedControl');
64
+ export const FileInput = c('FileInput');
65
+ // ---------- 导航 ----------
66
+ export const Anchor = c('Anchor');
67
+ export const NavLink = c('NavLink');
68
+ export const Tabs = Object.assign(c('Tabs'), { List: c('Tabs.List'), Tab: c('Tabs.Tab'), Panel: c('Tabs.Panel') });
69
+ export const Breadcrumbs = c('Breadcrumbs');
70
+ export const Pagination = c('Pagination');
71
+ export const Stepper = Object.assign(c('Stepper'), { Step: c('Stepper.Step') });
72
+ export const Burger = c('Burger');
73
+ // ---------- 反馈与浮层 ----------
74
+ export const Alert = c('Alert');
75
+ export const Loader = c('Loader');
76
+ export const Progress = c('Progress');
77
+ export const RingProgress = c('RingProgress');
78
+ export const Skeleton = c('Skeleton');
79
+ export const Notification = c('Notification');
80
+ /** 简单邮件卡片,正文可放 Button。 */
81
+ export const Email = c('Email');
82
+ /** 键值记录;fields 缺省时展示 value 的全部键,绑定的变化行显示前后对比。 */
83
+ export const DataCard = c('DataCard');
84
+ /** 按 states 顺序展示完成/当前/待完成状态,labels 可替换显示文字。 */
85
+ export const StateFlow = c('StateFlow');
86
+ export const Modal = c('Modal');
87
+ export const Drawer = c('Drawer');
88
+ // ---------- AI 交互(Joplo 自有) ----------
89
+ export const ChatMessage = c('ChatMessage');
90
+ export const ToolCall = c('ToolCall');
91
+ export const PromptInput = c('PromptInput');
92
+ /**
93
+ * 思考过程(D56):`<Thinking title={$.ui.thought} active={$.ui.thinking} duration="8 秒" opened>…</Thinking>`。
94
+ * title 是此刻在想什么(随步骤 set 变化);active 时标题闪动;结束后显示“思考了 {duration}”;opened 时展开子节点里的过程。
95
+ */
96
+ export const Thinking = c('Thinking');
97
+ /**
98
+ * Agent 的侧边产物(D56):`<Artifact title="露营行程.md" kind="doc" status={$.ui.planStatus} version="v2">…</Artifact>`,
99
+ * 子节点是产物内容(Text / List / Table / Code 等,文字可以用 stream 逐字生成)。
100
+ * 对话里引用它用 `<Artifact.Ref id="open_plan" title="露营行程" kind="doc" />`(可点击)。
101
+ */
102
+ export const Artifact = Object.assign(c('Artifact'), { Ref: c('Artifact.Ref') });
103
+ // ---------- 设备外框(Joplo 自有) ----------
104
+ /** 终端窗口:`<Terminal title="~/work"><Terminal.Command>npx joplo skill</Terminal.Command><Terminal.Output tone="ok">…</Terminal.Output></Terminal>`。 */
105
+ export const Terminal = Object.assign(c('Terminal'), { Command: c('Terminal.Command'), Output: c('Terminal.Output'), Input: c('Terminal.Input') });
106
+ /** 手机外框:子节点按手机宽度排版。页面写 viewport: 'mobile' 时舞台会自动套上,不用写它。 */
107
+ export const Phone = c('Phone');
108
+ // ---------- 产品界面(D68) ----------
109
+ /** 空状态 / 结果页:`<EmptyState icon="✉" title="还没有成员" description="邀请同事一起协作"><Button id="invite">邀请成员</Button></EmptyState>`。 */
110
+ export const EmptyState = c('EmptyState');
111
+ /** 列表行:`<ListRow avatar="阿杰" title="阿杰" description="已确认"><Badge>成员</Badge></ListRow>`;右侧内容放在子节点里。 */
112
+ export const ListRow = c('ListRow');
113
+ /** 指标卡:`<Stat label="本月注册" value={$.data.signups} diff="+12%" />`。 */
114
+ export const Stat = c('Stat');
115
+ /** 键值清单:`<KeyValue items={[{ label: '席位', value: '5 个' }, { label: '合计', value: '¥ 450' }]} total />`。 */
116
+ export const KeyValue = c('KeyValue');
117
+ /** 低保真图表(对齐 @mantine/charts):`<BarChart data={$.data.weeks} dataKey="week" series={[{ name: 'signups', label: '注册' }]} />`。 */
118
+ export const BarChart = c('BarChart');
119
+ export const LineChart = c('LineChart');
120
+ /**
121
+ * 菜单(对齐 Mantine):opened 写在 Menu 上,下拉只在为真时出现。
122
+ * `<Menu opened={$.ui.rowMenu}><Menu.Target><ActionIcon id="row_more" aria-label="更多">⋯</ActionIcon></Menu.Target>
123
+ * <Menu.Dropdown><Menu.Item id="revoke" color="red">撤销邀请</Menu.Item></Menu.Dropdown></Menu>`
124
+ */
125
+ export const Menu = Object.assign(c('Menu'), { Target: c('Menu.Target'), Dropdown: c('Menu.Dropdown'), Item: c('Menu.Item'), Label: c('Menu.Label'), Divider: c('Menu.Divider') });
126
+ /** 手机底部标签栏:`<TabBar id="tabs" aria-label="底部导航" value={$.ui.tab} data={[{ value: 'home', label: '首页', icon: '⌂' }]} />`,select 切换。 */
127
+ export const TabBar = c('TabBar');
128
+ // ---------- Joplo ----------
129
+ export const Placeholder = c('Placeholder');
@@ -0,0 +1,3 @@
1
+ export declare const skill = "# Standard UI: ui\n\nUse this plugin for application interfaces, forms, overlays, navigation, data displays, device frames and HTML fallback content. Install @joplo-app/plugin-ui, import ui from it and list ui in config.plugins. The engine enables nothing implicitly.\n\n## Start and verify\n- Import author markers directly from @joplo-app/plugin-ui and write normal TSX. The compiler emits ui:Button and other namespaced types; write HTML tags directly, which compile to ui:div, ui:span, etc.\n- Read joplo skill ui/components for the enabled catalog; joplo components Menu DataTable gives props, behavior groups and runnable author snippets. If two plugins share a short name, request the full namespaced name.\n- Give interactive elements stable ids. Describe their behavior with flow steps, disabled plus disabledReason, or unmodeled. Run check, then render the affected steps; passing schema validation alone does not prove the intended screen is visible.\n\n## State and visibility\n- type / select / toggle steps write inputs[id]. Use value, checked or defaultValue for initial visuals. Paused audiences can type (including multiline PromptInput), change Switch/Checkbox/Radio/Select/SegmentedControl, switch Tabs and expand Accordion locally; value/checked bindings synchronize local views. Playing, jumping or Reset discards these edits. A matching next scripted action wins; an action in another reachable scenario asks to switch.\n- Select accepts the displayed label or stored value; MultiSelect toggles membership. For a bound option list, ensure the list exists before the select step.\n- Modal and Drawer use opened, only; the old open alias is not supported. Menu forwards opened to its own dropdown; an explicitly set child value wins, and a nested menu owns its own state.\n- AppShell regions and Phone child width use the same declared layout data as playback. A narrow screen can hide a target even when when is true; check the actual viewport before changing the step.\n- Image accepts embedded project assets via asset('./photo.jpg'). URLs and ordinary paths intentionally draw a placeholder; fix IMAGE_PLACEHOLDER by embedding the file. fit accepts cover or contain.\n- Display properties are declared per component for translation. Identifiers, structural values and unrelated props are not automatically display text; for custom content, declare text or exact textPaths traits in its own plugin. For nested data, target titles/labels precisely; do not translate an entire configuration object containing colors or identifiers.\n\n## Migration and recovery\n- Plugin unavailable placeholders indicate a missing or mismatched runtime: reinstall matching package versions and rerun build. The ui catalog, runtime and author markers are checked together by the release smoke test. Runtime imports must be production dependencies or explicit shared host services: a working workspace or prebuilt web bundle does not prove the package runtime can load after a clean install.\n\nRelated topics: components, authoring, focus, check, plugins\n";
2
+ export declare const patterns = "# UI recipes and pitfalls\n\n## A prompt the audience can actually edit\nPromptInput renders a native textarea. Arbitrary text is disposable exploration and follows the same first-input pause, bound value synchronization and reset/resume rules as other text fields. Captures and disabled prompts remain read-only. Keep submission in a declared click/press step or free reaction; editing alone does not call a model or send anything. Test a non-script phrase with a real keyboard, because replayed .type() does not prove that audience input works.\n\n## Readable period interfaces\nMake the working interface the subject: enlarge input text, action targets and visible results together, rather than using a large year beside a tiny window. A compact chapter label and one source line can carry context; keep the full attribution/license inventory with the assets.\n\nFor a collected CSS device frame, retain only licensed generic shell parts and record the adaptations; a later frame library is not evidence of a historical model. When CSS or fonts are embedded into an offline export, retain the applicable full license notices in that exported asset as well as in the repository. Scope and match the original selector specificity when resizing a speaker or button, otherwise an apparently correct override can lose to a library descendant rule. Use the rendered control dimensions to verify the override.\n\nHTML text nodes may be wrapped in spans by this renderer. Avoid blanket descendant-span hiding to remove one note: it can also hide the main text. Remove the unwanted note from the authored tree or give that specific element a class. Device children with z-index can also overpaint a later sibling: give a transition scrim and its focal object explicit stacking levels and inspect the real pixels.\n\n## Styling a collected interface library\nVendor only the required, redistributable CSS primitives; scope them under an authored root class. Exclude unverified bundled fonts, icon files and logos rather than treating the stylesheet's license as permission for all assets. Record the exact extraction and selector changes alongside retained licenses.\n\nChoose --jp-stroke: clean in a project theme when CSS supplies historical borders and button bevels. UI components supply inline defaults for size, padding and color; a class alone may not win. Prefer documented props or explicit style overrides for ordinary cases. When a reusable vendor theme must own these values, narrowly scoped !important declarations can override just those defaults. Check computed styles, the actual rendered button and text contrast; do not globally change --jp-ink merely to tint a button.\n\nNative inputs are the editable surface; their displayed text is not a real backend. A local send reaction can set ui.sent while the result bubble reads inputs.<fieldId>, so the audience sees their own text without copying it into script data. Use a named option.free object, give each reaction a stable key, and retain the deterministic send effects in a scenario. Verify the same reaction in the actual built site as well as the board; a valid script does not prove that publication retained local reaction data. Test a non-script phrase: an exact next action can commit the script instead, and an exact action belonging to another scenario can prompt to switch.\n\n\nThese examples use this explicitly enabled plugin. Engine flow/state/review rules remain in the engine skill.\n\n## Controls over a visual story\n- Use Image with asset() for the actual downloaded artwork; keep source/creator/license/modification details beside the files. CSS crop/opacity changes still belong in that record. Do not use a remote URL as an offline-image substitute.\n- On a dark art background, Button's default filled variant may draw a Rough fill over your custom CSS. Use variant=\"subtle\" when authoring the whole surface, set text size/color explicitly, and choose --jp-stroke: clean in the project theme when appropriate. A class does not automatically override component inline font/color values. Verify contrast, focus and hitbox after the final composition.\n- Hover and dragging need a visible click alternative. A discrete timeline can use one draggable handle plus labeled endpoint buttons; keep the effects identical in both paths. Use a large enough drop zone and test it after the Canvas scales, not only at design resolution.\n\n### authoring: worked example\n```tsx\nimport { $, done, flow, option, page, set } from '@joplo-app/kit';\nimport { Alert, Button, Loader, Stack, TextInput } from '@joplo-app/plugin-ui';\nconst send = (name: string, cases: string[]) => flow(name, { start: '/reset', cases })\n .type('email', 'ana@exmple.com').click('send', 'Send the reset link', set($.ui.sending, true));\nexport default option({\n name: 'A \u00B7 Email link', idea: 'Send a reset link by email',\n state: { ui: { sending: false, sent: false, emailError: '' } },\n routes: { '/reset': page({ title: 'Reset password' }, <Stack>\n <TextInput id=\"email\" label=\"Email\" error={$.ui.emailError} />\n <Button id=\"send\" disabled={$.ui.sending} disabledReason=\"Sending\u2026\">Send link</Button>\n <Loader when={$.ui.sending} />\n <Alert id=\"sent\" color=\"green\" when={$.ui.sent}>Check your inbox</Alert>\n </Stack>) },\n scenarios: {\n unknown_email: send('Typo, then retry', ['unknown_email'])\n .system('No account for this email', set($.ui.sending, false), set($.ui.emailError, 'No account with this email'))\n .type('email', 'ana@example.com', set($.ui.emailError, ''))\n .click('send', 'Retry', set($.ui.sending, true))\n .system('Server accepts', set($.ui.sending, false), set($.ui.sent, true), done('Link sent after retry')),\n },\n});\n```\n\nFrom authoring:\nNative UI controls need no free declaration: Tabs, Accordion, text inputs, Switch, Checkbox, Radio, Select and SegmentedControl change local values. A bound checked/value prop updates locally too, keeping two views of the same state synchronized. Keep the scripted result in step effects; do not store audience input or credentials in the source.\n\nFrom authoring:\n- layouts/app.tsx: export default layout(<AppShell header={{ height: 56 }} navbar={{ width: 240, breakpoint: 'sm' }}><AppShell.Navbar>\u2026</AppShell.Navbar><AppShell.Main><Outlet/></AppShell.Main><Slot name=\"overlay\"/></AppShell>).\n\nFrom authoring:\n- asset('./photo.jpg'): reads an image next to the calling file and returns a data URI for <Image src> or a CSS backgroundImage (see components).\n\nFrom authoring:\n- on: { '/members': { actions: <Button id=\"invite\">Invite</Button> } }: add elements to a slot on an existing route.\n\nFrom authoring:\n- routes: { '/members/new': page({ title: 'New member' }, <Stack>\u2026</Stack>) }: add a route for this option.\n\nFrom authoring:\n- Props can bind: <TextInput id=\"email\" error={$.ui.emailError}/>, <Text>{$.data.name}</Text>.\n\nFrom authoring:\n- Overlays: <Modal id=\"dlg\" opened={$.ui.open} title=\"\u2026\">; Drawer works the same way. Only opened controls visibility.\n\nFrom authoring:\n- Use Stack / Group / SimpleGrid / Grid props: <Group justify=\"space-between\">, <SimpleGrid cols={{ base: 1, md: 3 }}>.\n\nFrom authoring:\n- Use responsive objects { base, xs, sm, md, lg, xl }, or visibleFrom=\"sm\" / hiddenFrom=\"sm\". AppShell collapses navigation below navbar.breakpoint.\n\nFrom authoring:\n- Set input labels directly: <TextInput id=\"email\" label=\"Email\"/>. Auto captions use them.\n\nFrom demo:\n- Drag begins on a declared drag target before stage pan/plugin orbit; Enter or Space on that target provides the scripted drag alternative. Pointerdown pauses immediately and lets the same drag finish. Hover never pauses playback, but matches normally while paused; touch activates hover by tapping and follows the pause-and-apply rule. Editable input keeps its first key, Space and arrow keys. The first wheel event pauses and scrolls; keep its inertia in the scroll area before stage zoom.\n\nFrom demo:\n- Each page has one device, desktop by default. page({ viewport: 'mobile' }, tree) uses a Phone frame at a fixed width of 390.\n\nFrom demo:\n- To show a phone turned sideways (a landscape app, a video player), write <Phone landscape> in a layer: the screen is 772 \u00D7 358 with the notch on the left.\n\nFrom demo:\n- Each layer tab shows an icon so viewers can tell layers apart when many tabs are squeezed: layer('Terminal', <Cli/>, { icon: 'terminal' }). Presets: window, browser, mobile, desktop, terminal, chat, agent, server, data, email, doc, flow. Default: mobile for mobile layers, otherwise window.\n\nFrom demo:\n <Grid h=\"100%\"><Grid.Col span={6}><Stack h=\"100%\"><ScrollArea style={{ flex: 1, minHeight: 0 }}>Messages</ScrollArea>\n\nFrom demo:\n <Group><PromptInput id=\"prompt\"/><Button id=\"send\">Send</Button></Group></Stack></Grid.Col></Grid>\n\nFrom demo:\n- The same applies to placeholders and artifacts: use h=\"100%\" on Placeholder; Artifact fills its container by default.\n\nFrom demo:\n- What gets squeezed: in an h=\"100%\" column, a Table, DataTable, or ScrollArea shrinks to the space left and scrolls inside. Rows stay in the DOM and check passes, but viewers see fewer rows.\n\nFrom demo:\n- .card('Title', { sub?, points?, role? }, ...effects): one large sentence filling the stage, as a step. Use for openings, endings, time jumps, and role switches.\n\nFrom demo:\n- Agent UI: <Thinking title={$.ui.thought} active={$.ui.thinking} duration=\"6 seconds\">Process</Thinking> shows thinking;\n\nFrom demo:\n <Artifact title=\"itinerary.md\" kind=\"doc\" status={$.ui.planStatus}>Content</Artifact> is a side artifact; reference it in the conversation with <Artifact.Ref id=\"\u2026\"/>.\n\nFrom demo:\n- Email: a simple external email card. The card is not interactive; buttons in its body use ordinary steps.\n\nFrom demo:\n Example: <Email from=\"Camping assistant\" to=\"Ajie\" subject=\"Itinerary updated\" time=\"Friday 20:05\">Body</Email>.\n\nFrom demo:\n- DataCard / StateFlow: place in a separate layer and switch to it with shot. Only these two components show this step's changes and before/after values. StateFlow marks earlier states completed according to states order.\n\nFrom demo:\n Example: layer('Itinerary data', <Stack><StateFlow value={$.data.trip.status} states={['draft','shared']} labels={{ draft: 'Draft', shared: 'Shared' }}/><DataCard title=\"Itinerary\" value={$.data.trip} fields={[{ key: 'status', label: 'Status' }]}/></Stack>).\n\nFrom demo:\n- Filled buttons default to --jp-ink, so changing --jp-ink also recolors body text. There is no primary-color variable yet: set the brand color on the button itself, <Button color=\"#e8590c\">, instead of overriding inline styles.\n\nFrom focus:\n Skipped on purpose: modals and drawers (their backdrop already says it), DataCard / StateFlow (they show their own change), more than 3 changes at once, a new page or layer, the same element changing again or another line appended to the same log, and any step where you wrote a focus yourself.\n\nFrom focus:\n- Secrets and private data: <Code redact>sk-live-\u2026</Code>, or redact={not($.ui.revealed)} to unblur after a step. Blurred, layout unchanged, on every step.\n\nFrom check:\n- Group props: label, icon (client, service, model, database, queue, storage, external, tool, user, cloud, gateway, lock, network, device, box, brain), tone (gray, blue, green, orange, red, purple, teal), group (parent id). Groups nest.\n\nFrom check:\n- IMAGE_PLACEHOLDER: <Image src> is a URL or plain path, which is never loaded. Put the file in the project and write src={asset('./photo.jpg')}.\n\nFrom check:\n- INVALID_IMAGE_FIT: <Image fit> accepts cover or contain.\n\nFrom check:\nAn element that looks usable (Button, TextInput, Select, NavLink, a, \u2026; see components) has an id but no step in any scenario of this option. Viewers will try it; decide what happens:\n\nFrom focus:\n Skipped on purpose: modals and drawers (their backdrop already says it), DataCard / StateFlow (they show their own change), more than 3 changes at once, a new page or layer, the same element changing again or another line appended to the same log, and any step where you wrote a focus yourself.\n\nFrom focus:\n- Secrets and private data: <Code redact>sk-live-\u2026</Code>, or redact={not($.ui.revealed)} to unblur after a step. Blurred, layout unchanged, on every step.\n\nCommon pitfall: Only catalog components and the listed HTML tags exist; <br/> is UNKNOWN_COMPONENT, so write 'Line 1\\nLine 2' inside <Text>\n\nCommon pitfall: UI and HTML belong to this plugin: import author markers directly from @joplo-app/plugin-ui, explicitly enable ui in config.plugins, and read ui/components. The engine has no bare-name or old-IR compatibility layer.";
3
+ export declare function componentGuide(docs: Record<string, string>, examples: Record<string, string>): string;
package/dist/guide.js ADDED
@@ -0,0 +1,182 @@
1
+ export const skill = `# Standard UI: ui
2
+
3
+ Use this plugin for application interfaces, forms, overlays, navigation, data displays, device frames and HTML fallback content. Install @joplo-app/plugin-ui, import ui from it and list ui in config.plugins. The engine enables nothing implicitly.
4
+
5
+ ## Start and verify
6
+ - Import author markers directly from @joplo-app/plugin-ui and write normal TSX. The compiler emits ui:Button and other namespaced types; write HTML tags directly, which compile to ui:div, ui:span, etc.
7
+ - Read joplo skill ui/components for the enabled catalog; joplo components Menu DataTable gives props, behavior groups and runnable author snippets. If two plugins share a short name, request the full namespaced name.
8
+ - Give interactive elements stable ids. Describe their behavior with flow steps, disabled plus disabledReason, or unmodeled. Run check, then render the affected steps; passing schema validation alone does not prove the intended screen is visible.
9
+
10
+ ## State and visibility
11
+ - type / select / toggle steps write inputs[id]. Use value, checked or defaultValue for initial visuals. Paused audiences can type (including multiline PromptInput), change Switch/Checkbox/Radio/Select/SegmentedControl, switch Tabs and expand Accordion locally; value/checked bindings synchronize local views. Playing, jumping or Reset discards these edits. A matching next scripted action wins; an action in another reachable scenario asks to switch.
12
+ - Select accepts the displayed label or stored value; MultiSelect toggles membership. For a bound option list, ensure the list exists before the select step.
13
+ - Modal and Drawer use opened, only; the old open alias is not supported. Menu forwards opened to its own dropdown; an explicitly set child value wins, and a nested menu owns its own state.
14
+ - AppShell regions and Phone child width use the same declared layout data as playback. A narrow screen can hide a target even when when is true; check the actual viewport before changing the step.
15
+ - Image accepts embedded project assets via asset('./photo.jpg'). URLs and ordinary paths intentionally draw a placeholder; fix IMAGE_PLACEHOLDER by embedding the file. fit accepts cover or contain.
16
+ - Display properties are declared per component for translation. Identifiers, structural values and unrelated props are not automatically display text; for custom content, declare text or exact textPaths traits in its own plugin. For nested data, target titles/labels precisely; do not translate an entire configuration object containing colors or identifiers.
17
+
18
+ ## Migration and recovery
19
+ - Plugin unavailable placeholders indicate a missing or mismatched runtime: reinstall matching package versions and rerun build. The ui catalog, runtime and author markers are checked together by the release smoke test. Runtime imports must be production dependencies or explicit shared host services: a working workspace or prebuilt web bundle does not prove the package runtime can load after a clean install.
20
+
21
+ Related topics: components, authoring, focus, check, plugins
22
+ `;
23
+ export const patterns = `# UI recipes and pitfalls
24
+
25
+ ## A prompt the audience can actually edit
26
+ PromptInput renders a native textarea. Arbitrary text is disposable exploration and follows the same first-input pause, bound value synchronization and reset/resume rules as other text fields. Captures and disabled prompts remain read-only. Keep submission in a declared click/press step or free reaction; editing alone does not call a model or send anything. Test a non-script phrase with a real keyboard, because replayed .type() does not prove that audience input works.
27
+
28
+ ## Readable period interfaces
29
+ Make the working interface the subject: enlarge input text, action targets and visible results together, rather than using a large year beside a tiny window. A compact chapter label and one source line can carry context; keep the full attribution/license inventory with the assets.
30
+
31
+ For a collected CSS device frame, retain only licensed generic shell parts and record the adaptations; a later frame library is not evidence of a historical model. When CSS or fonts are embedded into an offline export, retain the applicable full license notices in that exported asset as well as in the repository. Scope and match the original selector specificity when resizing a speaker or button, otherwise an apparently correct override can lose to a library descendant rule. Use the rendered control dimensions to verify the override.
32
+
33
+ HTML text nodes may be wrapped in spans by this renderer. Avoid blanket descendant-span hiding to remove one note: it can also hide the main text. Remove the unwanted note from the authored tree or give that specific element a class. Device children with z-index can also overpaint a later sibling: give a transition scrim and its focal object explicit stacking levels and inspect the real pixels.
34
+
35
+ ## Styling a collected interface library
36
+ Vendor only the required, redistributable CSS primitives; scope them under an authored root class. Exclude unverified bundled fonts, icon files and logos rather than treating the stylesheet's license as permission for all assets. Record the exact extraction and selector changes alongside retained licenses.
37
+
38
+ Choose --jp-stroke: clean in a project theme when CSS supplies historical borders and button bevels. UI components supply inline defaults for size, padding and color; a class alone may not win. Prefer documented props or explicit style overrides for ordinary cases. When a reusable vendor theme must own these values, narrowly scoped !important declarations can override just those defaults. Check computed styles, the actual rendered button and text contrast; do not globally change --jp-ink merely to tint a button.
39
+
40
+ Native inputs are the editable surface; their displayed text is not a real backend. A local send reaction can set ui.sent while the result bubble reads inputs.<fieldId>, so the audience sees their own text without copying it into script data. Use a named option.free object, give each reaction a stable key, and retain the deterministic send effects in a scenario. Verify the same reaction in the actual built site as well as the board; a valid script does not prove that publication retained local reaction data. Test a non-script phrase: an exact next action can commit the script instead, and an exact action belonging to another scenario can prompt to switch.
41
+
42
+
43
+ These examples use this explicitly enabled plugin. Engine flow/state/review rules remain in the engine skill.
44
+
45
+ ## Controls over a visual story
46
+ - Use Image with asset() for the actual downloaded artwork; keep source/creator/license/modification details beside the files. CSS crop/opacity changes still belong in that record. Do not use a remote URL as an offline-image substitute.
47
+ - On a dark art background, Button's default filled variant may draw a Rough fill over your custom CSS. Use variant="subtle" when authoring the whole surface, set text size/color explicitly, and choose --jp-stroke: clean in the project theme when appropriate. A class does not automatically override component inline font/color values. Verify contrast, focus and hitbox after the final composition.
48
+ - Hover and dragging need a visible click alternative. A discrete timeline can use one draggable handle plus labeled endpoint buttons; keep the effects identical in both paths. Use a large enough drop zone and test it after the Canvas scales, not only at design resolution.
49
+
50
+ ### authoring: worked example
51
+ \`\`\`tsx
52
+ import { $, done, flow, option, page, set } from '@joplo-app/kit';
53
+ import { Alert, Button, Loader, Stack, TextInput } from '@joplo-app/plugin-ui';
54
+ const send = (name: string, cases: string[]) => flow(name, { start: '/reset', cases })
55
+ .type('email', 'ana@exmple.com').click('send', 'Send the reset link', set($.ui.sending, true));
56
+ export default option({
57
+ name: 'A · Email link', idea: 'Send a reset link by email',
58
+ state: { ui: { sending: false, sent: false, emailError: '' } },
59
+ routes: { '/reset': page({ title: 'Reset password' }, <Stack>
60
+ <TextInput id="email" label="Email" error={$.ui.emailError} />
61
+ <Button id="send" disabled={$.ui.sending} disabledReason="Sending…">Send link</Button>
62
+ <Loader when={$.ui.sending} />
63
+ <Alert id="sent" color="green" when={$.ui.sent}>Check your inbox</Alert>
64
+ </Stack>) },
65
+ scenarios: {
66
+ unknown_email: send('Typo, then retry', ['unknown_email'])
67
+ .system('No account for this email', set($.ui.sending, false), set($.ui.emailError, 'No account with this email'))
68
+ .type('email', 'ana@example.com', set($.ui.emailError, ''))
69
+ .click('send', 'Retry', set($.ui.sending, true))
70
+ .system('Server accepts', set($.ui.sending, false), set($.ui.sent, true), done('Link sent after retry')),
71
+ },
72
+ });
73
+ \`\`\`
74
+
75
+ From authoring:
76
+ Native UI controls need no free declaration: Tabs, Accordion, text inputs, Switch, Checkbox, Radio, Select and SegmentedControl change local values. A bound checked/value prop updates locally too, keeping two views of the same state synchronized. Keep the scripted result in step effects; do not store audience input or credentials in the source.
77
+
78
+ From authoring:
79
+ - layouts/app.tsx: export default layout(<AppShell header={{ height: 56 }} navbar={{ width: 240, breakpoint: 'sm' }}><AppShell.Navbar>…</AppShell.Navbar><AppShell.Main><Outlet/></AppShell.Main><Slot name="overlay"/></AppShell>).
80
+
81
+ From authoring:
82
+ - asset('./photo.jpg'): reads an image next to the calling file and returns a data URI for <Image src> or a CSS backgroundImage (see components).
83
+
84
+ From authoring:
85
+ - on: { '/members': { actions: <Button id="invite">Invite</Button> } }: add elements to a slot on an existing route.
86
+
87
+ From authoring:
88
+ - routes: { '/members/new': page({ title: 'New member' }, <Stack>…</Stack>) }: add a route for this option.
89
+
90
+ From authoring:
91
+ - Props can bind: <TextInput id="email" error={$.ui.emailError}/>, <Text>{$.data.name}</Text>.
92
+
93
+ From authoring:
94
+ - Overlays: <Modal id="dlg" opened={$.ui.open} title="…">; Drawer works the same way. Only opened controls visibility.
95
+
96
+ From authoring:
97
+ - Use Stack / Group / SimpleGrid / Grid props: <Group justify="space-between">, <SimpleGrid cols={{ base: 1, md: 3 }}>.
98
+
99
+ From authoring:
100
+ - Use responsive objects { base, xs, sm, md, lg, xl }, or visibleFrom="sm" / hiddenFrom="sm". AppShell collapses navigation below navbar.breakpoint.
101
+
102
+ From authoring:
103
+ - Set input labels directly: <TextInput id="email" label="Email"/>. Auto captions use them.
104
+
105
+ From demo:
106
+ - Drag begins on a declared drag target before stage pan/plugin orbit; Enter or Space on that target provides the scripted drag alternative. Pointerdown pauses immediately and lets the same drag finish. Hover never pauses playback, but matches normally while paused; touch activates hover by tapping and follows the pause-and-apply rule. Editable input keeps its first key, Space and arrow keys. The first wheel event pauses and scrolls; keep its inertia in the scroll area before stage zoom.
107
+
108
+ From demo:
109
+ - Each page has one device, desktop by default. page({ viewport: 'mobile' }, tree) uses a Phone frame at a fixed width of 390.
110
+
111
+ From demo:
112
+ - To show a phone turned sideways (a landscape app, a video player), write <Phone landscape> in a layer: the screen is 772 × 358 with the notch on the left.
113
+
114
+ From demo:
115
+ - Each layer tab shows an icon so viewers can tell layers apart when many tabs are squeezed: layer('Terminal', <Cli/>, { icon: 'terminal' }). Presets: window, browser, mobile, desktop, terminal, chat, agent, server, data, email, doc, flow. Default: mobile for mobile layers, otherwise window.
116
+
117
+ From demo:
118
+ <Grid h="100%"><Grid.Col span={6}><Stack h="100%"><ScrollArea style={{ flex: 1, minHeight: 0 }}>Messages</ScrollArea>
119
+
120
+ From demo:
121
+ <Group><PromptInput id="prompt"/><Button id="send">Send</Button></Group></Stack></Grid.Col></Grid>
122
+
123
+ From demo:
124
+ - The same applies to placeholders and artifacts: use h="100%" on Placeholder; Artifact fills its container by default.
125
+
126
+ From demo:
127
+ - What gets squeezed: in an h="100%" column, a Table, DataTable, or ScrollArea shrinks to the space left and scrolls inside. Rows stay in the DOM and check passes, but viewers see fewer rows.
128
+
129
+ From demo:
130
+ - .card('Title', { sub?, points?, role? }, ...effects): one large sentence filling the stage, as a step. Use for openings, endings, time jumps, and role switches.
131
+
132
+ From demo:
133
+ - Agent UI: <Thinking title={$.ui.thought} active={$.ui.thinking} duration="6 seconds">Process</Thinking> shows thinking;
134
+
135
+ From demo:
136
+ <Artifact title="itinerary.md" kind="doc" status={$.ui.planStatus}>Content</Artifact> is a side artifact; reference it in the conversation with <Artifact.Ref id="…"/>.
137
+
138
+ From demo:
139
+ - Email: a simple external email card. The card is not interactive; buttons in its body use ordinary steps.
140
+
141
+ From demo:
142
+ Example: <Email from="Camping assistant" to="Ajie" subject="Itinerary updated" time="Friday 20:05">Body</Email>.
143
+
144
+ From demo:
145
+ - DataCard / StateFlow: place in a separate layer and switch to it with shot. Only these two components show this step's changes and before/after values. StateFlow marks earlier states completed according to states order.
146
+
147
+ From demo:
148
+ Example: layer('Itinerary data', <Stack><StateFlow value={$.data.trip.status} states={['draft','shared']} labels={{ draft: 'Draft', shared: 'Shared' }}/><DataCard title="Itinerary" value={$.data.trip} fields={[{ key: 'status', label: 'Status' }]}/></Stack>).
149
+
150
+ From demo:
151
+ - Filled buttons default to --jp-ink, so changing --jp-ink also recolors body text. There is no primary-color variable yet: set the brand color on the button itself, <Button color="#e8590c">, instead of overriding inline styles.
152
+
153
+ From focus:
154
+ Skipped on purpose: modals and drawers (their backdrop already says it), DataCard / StateFlow (they show their own change), more than 3 changes at once, a new page or layer, the same element changing again or another line appended to the same log, and any step where you wrote a focus yourself.
155
+
156
+ From focus:
157
+ - Secrets and private data: <Code redact>sk-live-…</Code>, or redact={not($.ui.revealed)} to unblur after a step. Blurred, layout unchanged, on every step.
158
+
159
+ From check:
160
+ - Group props: label, icon (client, service, model, database, queue, storage, external, tool, user, cloud, gateway, lock, network, device, box, brain), tone (gray, blue, green, orange, red, purple, teal), group (parent id). Groups nest.
161
+
162
+ From check:
163
+ - IMAGE_PLACEHOLDER: <Image src> is a URL or plain path, which is never loaded. Put the file in the project and write src={asset('./photo.jpg')}.
164
+
165
+ From check:
166
+ - INVALID_IMAGE_FIT: <Image fit> accepts cover or contain.
167
+
168
+ From check:
169
+ An element that looks usable (Button, TextInput, Select, NavLink, a, …; see components) has an id but no step in any scenario of this option. Viewers will try it; decide what happens:
170
+
171
+ From focus:
172
+ Skipped on purpose: modals and drawers (their backdrop already says it), DataCard / StateFlow (they show their own change), more than 3 changes at once, a new page or layer, the same element changing again or another line appended to the same log, and any step where you wrote a focus yourself.
173
+
174
+ From focus:
175
+ - Secrets and private data: <Code redact>sk-live-…</Code>, or redact={not($.ui.revealed)} to unblur after a step. Blurred, layout unchanged, on every step.
176
+
177
+ Common pitfall: Only catalog components and the listed HTML tags exist; <br/> is UNKNOWN_COMPONENT, so write 'Line 1\\nLine 2' inside <Text>
178
+
179
+ Common pitfall: UI and HTML belong to this plugin: import author markers directly from @joplo-app/plugin-ui, explicitly enable ui in config.plugins, and read ui/components. The engine has no bare-name or old-IR compatibility layer.`;
180
+ export function componentGuide(docs, examples) {
181
+ return ['# UI component catalog', 'Import these markers from @joplo-app/plugin-ui; enable ui in config.plugins.', ...Object.entries(docs).map(([name, doc]) => ['## ' + name, doc, ...(examples[name] ? ['', '```tsx', examples[name], '```'] : [])].join('\n'))].join('\n\n');
182
+ }
@@ -0,0 +1,5 @@
1
+ import { type PluginComponentSpec } from '@joplo-app/kit/plugin';
2
+ export declare const components: Record<string, PluginComponentSpec>;
3
+ declare const _default: import("@joplo-app/kit").PluginDef;
4
+ export default _default;
5
+ export * from './components.js';
package/dist/plugin.js ADDED
@@ -0,0 +1,66 @@
1
+ import packageInfo from './version.json' with { type: 'json' };
2
+ import { skill, patterns, componentGuide } from "./guide.js";
3
+ import { definePlugin } from '@joplo-app/kit/plugin';
4
+ import { COMPONENT_TYPES, COMPONENT_DOCS, COMPONENT_EXAMPLES, INTERACTIVE_TYPES, VALUE_TYPES, GATED_CONTAINERS, HTML_TAGS } from "./catalog.js";
5
+ // The UI family owns its vocabulary and display semantics, including HTML fallbacks.
6
+ const display = {
7
+ Divider: ['label'], Blockquote: ['cite'], Avatar: ['name'], Image: ['alt'], Indicator: ['label'],
8
+ 'Timeline.Item': ['title'], Table: ['data'], DataTable: ['records', 'noRecordsText'],
9
+ Radio: ['label'], 'Radio.Card': ['label', 'description', 'leftSection'], 'Radio.Group': ['label', 'description', 'value', 'error'],
10
+ 'Stepper.Step': ['label', 'description'], Alert: ['title'], Notification: ['title'], Modal: ['title'], Drawer: ['title'],
11
+ RingProgress: ['label'], ChatMessage: ['name'], ToolCall: ['title'], Thinking: ['title', 'duration'],
12
+ Artifact: ['title'], 'Artifact.Ref': ['title'], Email: ['from', 'to', 'subject', 'time'], DataCard: ['title', 'value'], StateFlow: ['labels'],
13
+ Terminal: ['title'], 'Terminal.Input': ['prompt', 'placeholder', 'value'], Placeholder: ['label'],
14
+ EmptyState: ['title', 'description'], ListRow: ['title', 'description', 'avatar'], Stat: ['label', 'value', 'diff', 'description'],
15
+ KeyValue: ['items'], BarChart: ['data', 'unit'], LineChart: ['data', 'unit'],
16
+ NavLink: ['label', 'description', 'leftSection', 'rightSection'], Button: ['leftSection', 'rightSection'],
17
+ 'Menu.Item': ['leftSection', 'rightSection'], 'Tabs.Tab': ['leftSection', 'rightSection'],
18
+ };
19
+ const textPaths = { DataTable: [['columns', '*', 'title']], DataCard: [['fields', '*', 'label']], BarChart: [['series', '*', 'label']], LineChart: [['series', '*', 'label']] };
20
+ const fieldText = ['label', 'description', 'placeholder', 'error', 'value'];
21
+ const traits = (name) => ({
22
+ ...(INTERACTIVE_TYPES.has(name) ? { hover: true } : {}),
23
+ ...(name === 'Tabs.Panel' ? { selectionGate: { owner: 'ui:Tabs', value: 'value', fallbackFirst: true } } : {}),
24
+ ...(name === 'Accordion.Panel' ? { selectionGate: { owner: 'ui:Accordion', item: 'ui:Accordion.Item', value: 'value' } } : {}),
25
+ ...(['TextInput', 'Textarea', 'PasswordInput', 'NumberInput', 'Autocomplete', 'PromptInput'].includes(name) ? { native: { action: 'type', prop: 'value' } } : {}),
26
+ ...(['Switch', 'Checkbox', 'Radio'].includes(name) ? { native: { action: 'toggle', prop: 'checked' } } : {}),
27
+ ...(['Tabs', 'Accordion', 'Radio.Group', 'Select', 'MultiSelect', 'SegmentedControl'].includes(name) ? { native: { action: 'select', prop: 'value', ...(name === 'Accordion' ? { deselect: true } : {}) } } : {}),
28
+ ...(textPaths[name] ? { textPaths: textPaths[name] } : {}),
29
+ text: ['aria-label', ...(display[name] ?? []), ...(VALUE_TYPES.has(name) ? fieldText : []), ...(['Select', 'MultiSelect', 'SegmentedControl', 'TabBar'].includes(name) ? ['data'] : [])],
30
+ ...(name === 'Indicator' ? { disabledPresentation: true } : {}),
31
+ ...(['Card', 'Paper', 'Modal', 'Drawer', 'Alert', 'Notification', 'Email', 'DataCard', 'StateFlow', 'Artifact', 'Terminal', 'ChatMessage', 'Table', 'DataTable', 'Accordion.Item', 'Tabs.Panel', 'AppShell.Navbar', 'AppShell.Aside', 'AppShell.Header', 'AppShell.Footer'].includes(name) ? { frame: true } : {}),
32
+ ...(VALUE_TYPES.has(name) ? { input: name === 'MultiSelect' ? 'multiple' : 'single' } : {}),
33
+ ...(GATED_CONTAINERS.has(name) ? { gate: ['opened'], selfEvident: true } : {}),
34
+ ...(['DataCard', 'StateFlow'].includes(name) ? { selfEvident: true } : {}),
35
+ ...(name === 'Checkbox' ? { toggleCaption: 'check' } : {}),
36
+ ...(['Select', 'MultiSelect', 'SegmentedControl', 'TabBar'].includes(name) ? { choices: { prop: 'data' } } : {}),
37
+ ...(name === 'Radio.Group' ? { choices: { descendants: ['ui:Radio', 'ui:Radio.Card'] } } : {}),
38
+ ...(name === 'Tabs' ? { choices: { descendants: ['ui:Tabs.Tab'] } } : {}),
39
+ ...(name === 'Accordion' ? { choices: { descendants: ['ui:Accordion.Item'], labelChild: 'ui:Accordion.Control' } } : {}),
40
+ ...(name === 'Pagination' ? { choices: { range: 'total' } } : {}),
41
+ ...(name === 'AppShell' ? { layout: { header: { size: 'height', defaultSize: 60 }, footer: { size: 'height', defaultSize: 60 }, navbar: { size: 'width', defaultSize: 260, breakpoint: true }, aside: { size: 'width', defaultSize: 300, breakpoint: true } } } : {}),
42
+ ...(name === 'AppShell.Navbar' ? { region: 'navbar' } : {}),
43
+ ...(name === 'AppShell.Aside' ? { region: 'aside' } : {}),
44
+ ...(name === 'Phone' ? { childWidth: { value: 390, when: { prop: 'landscape', value: true, width: 772 } } } : {}),
45
+ });
46
+ export const components = Object.fromEntries(COMPONENT_TYPES.map(name => [name, {
47
+ doc: COMPONENT_DOCS[name],
48
+ ...(COMPONENT_EXAMPLES[name] ? { example: COMPONENT_EXAMPLES[name] } : {}),
49
+ ...(HTML_TAGS.includes(name) ? { intrinsic: name } : {}),
50
+ ...(INTERACTIVE_TYPES.has(name) ? { interactive: true } : {}),
51
+ traits: traits(name),
52
+ ...(name === 'Menu' ? { inherit: [{ target: 'ui:Menu.Dropdown', props: ['opened'] }] } : {}),
53
+ }]));
54
+ export default definePlugin({ packageName: '@joplo-app/plugin-ui',
55
+ id: 'ui', title: 'Standard UI', version: packageInfo.version, runtime: new URL(import.meta.url.endsWith('.ts') ? './runtime.tsx' : './runtime.js', import.meta.url), skill, skillTopics: { components: { description: 'Component props and author examples', body: componentGuide(COMPONENT_DOCS, COMPONENT_EXAMPLES) }, patterns: { description: 'UI recipes and individual pitfalls', body: patterns } }, components,
56
+ compile: { node(n, { report }) {
57
+ if (n.type !== 'ui:Image')
58
+ return;
59
+ const { src, fit } = n.props;
60
+ if (typeof src === 'string' && src && !src.startsWith('data:image/'))
61
+ report('IMAGE_PLACEHOLDER', `<ui:Image src="${src.length > 60 ? `${src.slice(0, 57)}...` : src}"> is drawn as a placeholder: only embedded images are shown. Put the file in the project and write src={asset('./photo.jpg')} (path relative to this file; import asset from @joplo-app/kit)`, n.source, 'warning');
62
+ if (fit !== undefined && !(typeof fit === 'string' && ['cover', 'contain'].includes(fit)))
63
+ report('INVALID_IMAGE_FIT', `<ui:Image> fit="${String(fit)}" is not supported (available: cover, contain)`, n.source);
64
+ } },
65
+ });
66
+ export * from "./components.js";
@@ -0,0 +1,67 @@
1
+ import { type CSSProperties, type ReactNode } from 'react';
2
+ export declare const PHONE_LANDSCAPE: {
3
+ width: number;
4
+ height: number;
5
+ };
6
+ /** 内嵌的图片(asset() 生成的 data:image/… URI)才真正画出来;网址、路径不去加载(编译期会提示),仍画占位。 */
7
+ export declare const embeddedImage: (src: unknown) => string | null;
8
+ /** 选中的行:selectedRecords 里的 id(或带 idAccessor 的记录)。 */
9
+ export declare function selectedIds(selected: unknown, idAccessor: string): Set<string>;
10
+ /** Notification 的浮动位置(D68):没写 position 时内联;写了时浮在当前页面(弹窗层)的那个角或那条边上。 */
11
+ export declare const NOTIFICATION_POSITIONS: readonly ["top", "top-left", "top-right", "bottom", "bottom-left", "bottom-right"];
12
+ export declare function notificationPlacement(position: unknown): CSSProperties | null;
13
+ /** 手机外框的几何尺寸:机身边框,以及屏幕内的安全区(顶部避开听筒与圆角、底部避开圆角与横条、左右避开圆角)。 */
14
+ export declare const PHONE_BEZEL = 12;
15
+ export declare const PHONE_SAFE: {
16
+ readonly top: 40;
17
+ readonly bottom: 26;
18
+ readonly x: 12;
19
+ };
20
+ /** 横放(刘海在左)时屏幕内的安全区:左侧避开刘海,右侧与上下避开圆角,底部避开横条。 */
21
+ export declare const PHONE_SAFE_LANDSCAPE: {
22
+ readonly top: 12;
23
+ readonly bottom: 20;
24
+ readonly left: 44;
25
+ readonly right: 28;
26
+ };
27
+ /** 外框占用的总尺寸(含侧键余量),内容区为 width × height;横放时侧键在机身上下沿。 */
28
+ export declare function phoneFrameSize(width: number, height: number, landscape?: boolean): {
29
+ width: number;
30
+ height: number;
31
+ };
32
+ /**
33
+ * 手机外框(Phone 组件与舞台的自动外框共用):大圆角机身、侧边按键、顶部听筒、底部横条,不显示任何文字。
34
+ * children 放在屏幕安全区以内的内容区,内容区正好是 width × height(断点按 width 计算,不被圆角和听筒遮挡);
35
+ * scale 只做视觉缩放。主题取外层 ThemeContext(sketch 用手绘描边)。
36
+ */
37
+ export declare function PhoneFrame({ width, height, scale, children, seed, landscape }: {
38
+ width: number;
39
+ height: number;
40
+ scale?: number;
41
+ children?: ReactNode;
42
+ seed?: string; /** 横放:刘海在左,侧键在机身上下沿。 */
43
+ landscape?: boolean;
44
+ }): import("react").JSX.Element;
45
+ /** diff 的颜色:以 - / − 开头为红,以 + 开头为绿,其余跟随 color。 */
46
+ export declare function diffTone(diff: string): 'up' | 'down' | 'flat';
47
+ export type ChartModel = {
48
+ labels: string[];
49
+ series: {
50
+ name: string;
51
+ label: string;
52
+ color: string;
53
+ values: number[];
54
+ }[];
55
+ max: number;
56
+ ticks: number[];
57
+ };
58
+ /** 图表的纯数据部分:每个系列取 data[i][name](不是数字的当作 0),刻度取“好看”的整数步长。 */
59
+ export declare function chartModel(data: unknown, dataKey: unknown, series: unknown): ChartModel;
60
+ /** 下拉相对 Menu 的位置:top-* 在上方,*-end 右对齐。 */
61
+ export declare function menuPlacement(position: string): {
62
+ top: boolean;
63
+ end: boolean;
64
+ };
65
+ export declare const IMPLEMENTED_TYPES: string[];
66
+ declare const _default: import("@joplo-app/kit/plugin").PluginRuntime;
67
+ export default _default;