@parallelworks/ui 0.54.0 → 0.56.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.
Files changed (50) hide show
  1. package/README.md +65 -26
  2. package/dist/access/AccessDrawer.d.ts +30 -19
  3. package/dist/access/AccessDrawer.js +92 -67
  4. package/dist/access/AccessList.d.ts +17 -16
  5. package/dist/access/AccessList.js +218 -114
  6. package/dist/access/GrantView.d.ts +9 -8
  7. package/dist/access/GrantView.js +34 -36
  8. package/dist/access/PrincipalPicker.d.ts +38 -0
  9. package/dist/access/{GroupPicker.js → PrincipalPicker.js} +173 -80
  10. package/dist/access/index.d.ts +1 -1
  11. package/dist/access/model.d.ts +67 -24
  12. package/dist/access/model.js +111 -47
  13. package/dist/access/strings.d.ts +63 -39
  14. package/dist/access/strings.js +114 -84
  15. package/dist/access/types.d.ts +32 -14
  16. package/dist/ai/adapter/mock/mockAdapter.js +1 -1
  17. package/dist/ai/core/useSSEStream.d.ts +1 -1
  18. package/dist/components/Checkbox.js +1 -1
  19. package/dist/components/Combobox.js +25 -20
  20. package/dist/components/DatePicker.js +62 -58
  21. package/dist/components/Drawer.js +3 -1
  22. package/dist/components/Field.d.ts +8 -3
  23. package/dist/components/Field.js +32 -12
  24. package/dist/components/HoverCard.js +15 -10
  25. package/dist/components/InfoTip.d.ts +3 -1
  26. package/dist/components/InfoTip.js +2 -1
  27. package/dist/components/Menu.js +3 -0
  28. package/dist/components/Modal.js +32 -27
  29. package/dist/components/Popover.d.ts +4 -1
  30. package/dist/components/Popover.js +31 -26
  31. package/dist/components/Radio.js +1 -1
  32. package/dist/components/Select.js +47 -42
  33. package/dist/components/Slider.js +3 -3
  34. package/dist/components/Switch.js +1 -1
  35. package/dist/components/Tooltip.js +16 -11
  36. package/dist/components/fieldBox.js +1 -1
  37. package/dist/components/fullscreen.d.ts +4 -0
  38. package/dist/components/fullscreen.js +18 -0
  39. package/dist/editor/EditorField.js +1 -2
  40. package/dist/editor/Monaco.js +26 -2
  41. package/dist/editor/themes.js +1 -1
  42. package/dist/graph/GraphEditorDialogs.d.ts +1 -1
  43. package/dist/index.d.ts +1 -1
  44. package/dist/index.js +2 -2
  45. package/dist/styles/theme.css +10 -10
  46. package/dist/styles.css +1 -1
  47. package/dist/theme/derive.d.ts +2 -0
  48. package/dist/theme/derive.js +25 -14
  49. package/package.json +2 -2
  50. package/dist/access/GroupPicker.d.ts +0 -35
@@ -4,86 +4,118 @@ import { useMemo } from "react";
4
4
  //#region src/access/strings.tsx
5
5
  var plural = new Intl.PluralRules("en");
6
6
  var list = new Intl.ListFormat("en", { type: "conjunction" });
7
+ /** Counts side by side, as a label lists them: "Everyone, 3 people, 7 groups". */
8
+ var units = new Intl.ListFormat("en", {
9
+ type: "unit",
10
+ style: "short"
11
+ });
7
12
  /** A count and its noun: "1 group", "1,024 groups". */
8
13
  var countOf = (n, one, other) => `${n.toLocaleString()} ${plural.select(n) === "one" ? one : other}`;
9
14
  var and = (names) => list.format(names);
10
15
  var has = (names) => names.length === 1 ? "has" : "have";
16
+ var who = (people, groups, everyone = false) => [
17
+ everyone && "Everyone",
18
+ people > 0 && countOf(people, "person", "people"),
19
+ groups > 0 && countOf(groups, "group", "groups")
20
+ ].filter((part) => typeof part === "string");
11
21
  var ACCESS_STRINGS = {
12
22
  title: "Access",
13
- /** Under the title: the resource, and how many groups have a role on it. */
14
- summary: (resource, groups, everyone) => everyone ? groups === 0 ? `${resource} · Everyone has access` : `${resource} · Everyone and ${countOf(groups, "group have access", "groups have access")}` : groups === 0 ? `${resource} · No groups have access` : `${resource} · ${countOf(groups, "group has access", "groups have access")}`,
23
+ /** Under the title: the resource, and who has a role on it. */
24
+ summary: (resource, people, groups, everyone) => {
25
+ const parts = who(people, groups, everyone);
26
+ return `${resource} · ${parts.length === 0 ? "No access granted" : units.format(parts)}`;
27
+ },
15
28
  /** Names the switch between the two ways of listing who has access. */
16
29
  arrange: "List access",
17
30
  byRole: "By role",
18
- byGroup: "By group",
19
- filter: "Filter by role or group",
31
+ byPrincipal: "By person or group",
32
+ filter: "Filter by role, person or group",
20
33
  grantAccess: "Grant access",
21
- highImpact: "High impact",
22
- loading: "Loading who has access",
23
- groups: (n) => countOf(n, "group", "groups"),
24
- /** How many of a role's groups a filter left. */
25
- groupsShown: (shown, total) => `${shown.toLocaleString()} of ${countOf(total, "group", "groups")}`,
34
+ privileged: "Privileged",
35
+ loading: "Loading access",
36
+ /** Who holds a role: "1 person, 2 groups". */
37
+ holders: (people, groups) => units.format(who(people, groups)),
38
+ /** How many of a role's holders a filter left. */
39
+ holdersShown: (shown, total) => `${shown.toLocaleString()} of ${total.toLocaleString()}`,
26
40
  members: (n) => countOf(n, "member", "members"),
27
41
  people: (n) => countOf(n, "person", "people"),
28
42
  /** Everyone, where it is short: a tag or a picker row. */
29
43
  everyone: "Everyone",
30
44
  /** Everyone, where it stands as a row of its own. */
31
45
  everyoneInOrganization: "Everyone in your organization",
32
- everyoneJoinLater: "Including people who join later",
33
- /** Read with the badges a group row folds away: "+2". */
46
+ everyoneJoinLater: "Includes people who join later",
47
+ owner: "Owner",
48
+ /** Under the owner section and row. */
49
+ ownerDescription: "All permissions, can’t be removed",
50
+ /** Read with the badges a row folds away: "+2". */
34
51
  moreRoles: (n) => `+${n.toLocaleString()}`,
35
52
  moreRolesLabel: (roles) => `Also ${and(roles)}`,
36
- /** Beside a group whose other role gives everything this one does. */
37
- coveredBy: (roles) => `${and(roles)} covers this too`,
38
- /** Beside a group that has this through everyone as well. */
39
- everyoneHasThisToo: "Everyone has this too",
40
- /** A role a group holds that another of its roles already gives. */
41
- redundant: (roles) => `Redundant: ${and(roles)} already covers this.`,
42
- /** A role a group has only because everyone does. */
43
- throughEveryone: "Everyone in your organization has it",
44
- removeRole: (group, role) => `Remove ${group} from ${role}`,
45
- grantMoreRoles: "Grant more roles",
53
+ /** Beside someone whose other role gives everything this one does. */
54
+ coveredBy: (roles) => `Included in ${and(roles)}`,
55
+ /** Beside the owner, who has every role's permissions anyway. */
56
+ ownerHasAll: "Included in Owner",
57
+ /** Beside someone who has this through everyone as well. */
58
+ everyoneHasThisToo: "Also via Everyone",
59
+ /** Beside someone who has this through their groups, or an inherited role, as well. */
60
+ throughToo: (from) => `Also via ${and(from)}`,
61
+ /** A role someone holds that another of their roles already gives. */
62
+ redundant: (roles) => `Redundant, included in ${and(roles)}`,
63
+ redundantOwner: "Redundant, included in Owner",
64
+ /** A role someone has only because everyone does. */
65
+ throughEveryone: "Via Everyone",
66
+ /** A role someone has through one of their groups, or an inherited role. */
67
+ through: (from) => `Via ${from}`,
68
+ /** A role granted somewhere else, which can't be removed here. */
69
+ inheritedFrom: (from) => `Inherited from ${from}`,
70
+ removeRole: (principal, role) => `Remove ${principal} from ${role}`,
71
+ grantMoreRoles: "Grant roles",
46
72
  removeAllAccess: "Remove all access",
47
- removeTitle: (group, roles) => `Remove ${group} from ${and(roles)}?`,
48
- removeAllTitle: (group) => `Remove all of ${group}’s access?`,
49
- /** Leads the list of what a group loses; `members` is undefined when the count isn't known. */
50
- losesIntro: (members, everyone) => everyone ? "People who have it only through everyone will no longer be able to:" : members === void 0 ? "Its members will no longer be able to:" : `Its ${countOf(members, "member", "members")} will no longer be able to:`,
51
- /** After the list: why these are lost, and what they keep. */
52
- keeps: (own, throughEveryone) => [
53
- "No other role of theirs gives these.",
54
- own.length > 0 && `They keep ${and(own)}.`,
55
- throughEveryone.length > 0 && `They keep ${and(throughEveryone)}, which everyone has.`
56
- ].filter(Boolean).join(" "),
57
- /** When the roles it keeps give everything the removed ones did. */
58
- losesNothing: (own, throughEveryone) => `They lose nothing: ${and([...own, ...throughEveryone])} still ${own.length + throughEveryone.length === 1 ? "gives" : "give"} them all of it.`,
73
+ removeTitle: (principal, roles) => `Remove ${principal} from ${and(roles)}?`,
74
+ removeAllTitle: (principal) => `Remove all access for ${principal}?`,
75
+ /** Heads the list of what is lost; `members` is undefined when a group's size isn't known. */
76
+ losesIntro: (kind, members) => kind === "everyone" ? "Permissions lost by people without other access" : kind === "group" && members !== void 0 ? `Permissions lost by ${countOf(members, "member", "members")}` : "Permissions lost",
77
+ /** After the list: what they keep, their own roles and those through their groups, inherited
78
+ * roles or everyone. Empty when they keep nothing. */
79
+ keeps: (own, throughEveryone, through) => {
80
+ const kept = [
81
+ ...own,
82
+ ...through.flatMap(({ from, roles }) => roles.map((role) => `${role} via ${from}`)),
83
+ ...throughEveryone.map((role) => `${role} via Everyone`)
84
+ ];
85
+ return kept.length > 0 ? `Still has ${and(kept)}` : "";
86
+ },
87
+ /** When the roles they keep give everything the removed ones did. */
88
+ losesNothing: (roles) => `No permissions lost. Still covered by ${and(roles)}.`,
89
+ ownerLosesNothing: "No permissions lost. Owners have all permissions.",
59
90
  confirmRemove: (roles) => roles.length === 1 ? `Remove ${roles[0]}` : "Remove all access",
60
- removeFailed: (group, roles) => `Couldn’t remove ${group} from ${and(roles)}`,
61
- grantFailed: (groups, roles) => `Couldn’t grant ${and(roles)} to ${and(groups)}`,
91
+ removeFailed: (principal, roles) => `Couldn’t remove ${principal} from ${and(roles)}`,
92
+ grantFailed: (principals, roles) => `Couldn’t grant ${and(roles)} to ${and(principals)}`,
62
93
  undoFailed: "Couldn’t undo the change",
63
94
  /** Under a failed save; `reason` is the host's error message, when it gave one. */
64
95
  nothingChanged: (reason) => reason ? `${reason.replace(/\.?$/, ".")} Nothing changed.` : "Nothing changed.",
65
96
  /** The toast after a grant. */
66
- granted: (groups, roles) => `Gave ${and(groups)} ${and(roles)}.`,
97
+ granted: (principals, roles) => `Granted ${and(roles)} to ${and(principals)}`,
67
98
  /** The toast after a removal. */
68
- removed: (groups, roles) => `Removed ${and(groups)} from ${and(roles)}.`,
69
- undone: "Change undone.",
70
- readOnly: "You can see who has access.",
99
+ removed: (principals, roles) => `Removed ${and(principals)} from ${and(roles)}`,
100
+ undone: "Change undone",
101
+ readOnly: "View only.",
71
102
  /** After `readOnly` when the host gives no reason. */
72
- readOnlyReason: "Only people who manage access here can change it.",
73
- emptyTitle: "No groups have access yet",
74
- emptyDescription: (resource) => `Give a group a role so its members can see, use or manage ${resource}.`,
75
- noMatches: "No roles or groups match",
103
+ readOnlyReason: "Only people who manage access can make changes.",
104
+ emptyTitle: "No access granted",
105
+ emptyDescription: (resource) => `Grant a role to a person or group to give them access to ${resource}.`,
106
+ noMatches: "No matches",
76
107
  clearFilter: "Clear filter",
77
- back: "Back to who has access",
78
- groupsLabel: "Groups",
79
- addGroup: "Add a group",
80
- /** Under the groups field; `count` is undefined when the host doesn't say how many there are. */
81
- groupsHint: (count, everyone) => count === void 0 ? everyone ? "Search groups, or pick Everyone in your organization." : "Search groups." : `Search ${countOf(count, "group", "groups")}${everyone ? ", or pick Everyone in your organization" : ""}.`,
82
- everyoneCoversGroups: "Everyone covers every group, so there’s no need to add others.",
108
+ back: "Back to access",
109
+ principalsLabel: "People and groups",
110
+ addPrincipal: "Add people or groups",
111
+ /** Under the people and groups field; a count is undefined when the host doesn't give it. */
112
+ pickerHint: (people, groups, everyone) => `Search ${and([people === void 0 ? "people" : countOf(people, "person", "people"), groups === void 0 ? "groups" : countOf(groups, "group", "groups")])}${everyone ? ", or choose Everyone" : ""}`,
113
+ everyoneCoversGroups: "Everyone includes all people and groups",
83
114
  organization: "Organization",
84
115
  recent: "Recent",
85
- allGroups: "All groups",
86
- /** On a group's row in the picker: the roles it holds here. */
116
+ peopleSection: "People",
117
+ groupsSection: "Groups",
118
+ /** On a row in the picker: the roles it holds here. */
87
119
  hasRoles: (roles) => `has ${and(roles)}`,
88
120
  /** Read with a group's row: its size, where it comes from and its roles here. */
89
121
  groupSummary: (members, everyone, source, roles) => [
@@ -91,52 +123,50 @@ var ACCESS_STRINGS = {
91
123
  source === "synced" && "synced",
92
124
  roles.length > 0 ? `has ${and(roles)} here` : "no roles here"
93
125
  ].filter(Boolean).join(", "),
126
+ /** Read with a person's row: what tells them apart, and their roles here. */
127
+ personSummary: (detail, owner, roles) => [detail, owner ? "owner" : roles.length > 0 ? `has ${and(roles)} here` : "no roles here"].filter(Boolean).join(", "),
94
128
  rolesLabel: "Roles",
95
129
  searchRoles: "Search roles",
96
130
  /** On a role's row in the picker. */
97
- alreadyHave: (groups) => `${and(groups)} already ${has(groups)} it.`,
98
- alreadyCovered: (roles) => `${and(roles)} already covers this.`,
99
- everyoneHasIt: "Everyone already has it.",
131
+ alreadyHave: (principals) => `Already granted to ${and(principals)}`,
132
+ alreadyCovered: (roles) => `Included in ${and(roles)}`,
133
+ everyoneHasIt: "Already granted to Everyone",
100
134
  /** The footer's account of what pressing Grant does. */
101
- grantSummary: (groups, roles) => /* @__PURE__ */ jsxs(Fragment, { children: [
102
- /* @__PURE__ */ jsx("strong", {
103
- className: "font-medium text-(--theme-app)",
104
- children: and(groups)
105
- }),
106
- " ",
107
- groups.length === 1 ? "gets" : "get",
108
- " ",
109
- and(roles),
110
- "."
111
- ] }),
112
- /** Why Grant can't be pressed: every group has every role already. */
113
- nothingToGrant: (groups, roles) => `${and(groups)} already ${has(groups)} ${and(roles)}, so there’s nothing to grant.`,
114
- pickGroupsAndRoles: "Pick the groups and the roles to grant.",
115
- pickGroups: "Pick a group to grant to.",
116
- pickRoles: "Pick a role to grant.",
135
+ grantSummary: (principals, roles) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("strong", {
136
+ className: "font-medium text-(--theme-app)",
137
+ children: and(roles)
138
+ }), ` for ${and(principals)}`] }),
139
+ /** Why Grant can't be pressed: everyone picked has every role picked already. */
140
+ nothingToGrant: (principals, roles) => `${and(principals)} already ${has(principals)} ${and(roles)}`,
141
+ pickPrincipalsAndRoles: "Choose people or groups and roles",
142
+ pickPrincipals: "Choose people or groups",
143
+ pickRoles: "Choose roles",
117
144
  grant: (roles) => roles.length === 0 ? "Grant" : roles.length === 1 ? `Grant ${roles[0]}` : `Grant ${countOf(roles.length, "role", "roles")}`,
118
145
  grantToEveryone: "Grant to everyone",
119
146
  everyoneWarningTitle: (roles, people) => people === void 0 ? `${and(roles)} for everyone` : `${and(roles)} for all ${countOf(people, "person", "people")}`,
120
- everyoneWarning: (resource) => `Everyone in your organization, and everyone who joins later, will have it on ${resource}.`,
147
+ everyoneWarning: (resource) => `Everyone in your organization gets access to ${resource}, including people who join later.`,
121
148
  confirmEveryoneTitle: (roles, people) => people === void 0 ? `Grant ${and(roles)} to everyone?` : `Grant ${and(roles)} to all ${countOf(people, "person", "people")}?`,
122
- synced: "Synced from your identity provider",
149
+ synced: "Synced from identity provider",
123
150
  syncedShort: "Synced",
124
- manual: "Members added by hand",
125
- wholeOrganization: "Your whole organization",
126
- joinLater: "And anyone who joins later gets it automatically.",
151
+ manual: "Managed manually",
152
+ wholeOrganization: "Entire organization",
153
+ joinLater: "Includes people who join later",
154
+ /** Heads the groups a person is in that hold a role here. */
155
+ inGroups: "Groups with access",
127
156
  /** The first few members by name, and how many more there are. */
128
157
  memberNames: (names, more) => more > 0 ? and([...names, `${more.toLocaleString()} more`]) : and(names),
129
158
  moreMembers: (n) => `+${n.toLocaleString()}`,
130
- onResource: (resource) => `On ${resource}`,
131
- noRolesHere: "No roles here yet.",
132
- /** When a role being granted gives everything one the group holds does. */
133
- becomesRedundant: (granting, held) => `${and(granting)} also ${granting.length === 1 ? "gives" : "give"} them ${and(held)}, so ${and(held)} becomes redundant. Nothing is removed.`,
134
- alreadyHas: (roles) => `Already has ${and(roles)}.`,
135
- everyoneReach: (roles, people) => `${and(roles)} for everyone reaches ${people === void 0 ? "every person" : `all ${countOf(people, "person", "people")}`} in your organization, and anyone who joins later. You’ll be asked to confirm.`,
159
+ /** Heads the roles the highlighted one holds on this resource. */
160
+ currentRoles: "Current roles",
161
+ noRolesHere: "No roles",
162
+ /** When a role being granted gives everything one already held does. */
163
+ becomesRedundant: (granting, held) => `${and(held)} will be redundant, included in ${and(granting)}`,
164
+ alreadyHas: (roles) => `Already has ${and(roles)}`,
165
+ everyoneReach: (roles, people) => `Grants ${and(roles)} to ${people === void 0 ? "everyone" : `all ${countOf(people, "person", "people")}`}, including people who join later. Requires confirmation.`,
136
166
  updated: (ago) => `Membership updated ${ago}`,
137
- detailsFailed: "Couldn’t load this group’s details.",
138
- loadingDetails: "Loading the group’s details",
139
- noHighlight: "Highlight a group to see who is in it."
167
+ detailsFailed: "Couldn’t load group details",
168
+ loadingDetails: "Loading group details",
169
+ noHighlight: "Highlight a person or group to see details"
140
170
  };
141
171
  /** The access drawer's strings: the host's translations over the English defaults. */
142
172
  function useAccessStrings() {
@@ -1,5 +1,6 @@
1
1
  import type { TimeValue } from '../format.js';
2
- /** A named set of permissions. Groups are granted roles, never permissions one by one. */
2
+ /** A named set of permissions. People and groups are granted roles, never permissions one by
3
+ * one. */
3
4
  export interface AccessRole {
4
5
  id: string;
5
6
  name: string;
@@ -8,31 +9,48 @@ export interface AccessRole {
8
9
  /** The permissions it gives. Two roles that share some are allowed, and only redundant: the
9
10
  * drawer works out the overlap from these and says so as a hint. */
10
11
  permissions: readonly string[];
11
- /** Marks a role that reaches far, such as root on every node. It carries a tag, and granting
12
- * it to everyone says so before asking to confirm. */
13
- highImpact?: boolean | undefined;
12
+ /** Marks a role with administrative or root-level control. The role picker labels it, and
13
+ * granting it to everyone confirms with a destructive button. */
14
+ privileged?: boolean | undefined;
15
+ /** False for a role held here only through somewhere else, such as an organization's
16
+ * administrator role: it shows where someone holds it, and isn't offered to grant. */
17
+ grantable?: boolean | undefined;
14
18
  }
15
- /** A permission's name in words, for what a group loses when a role is taken away. */
19
+ /** A permission's name in words, for what someone loses when a role is taken away. */
16
20
  export interface AccessPermission {
17
21
  id: string;
18
22
  /** What it lets someone do, as a phrase that follows "will no longer be able to": "Sign in". */
19
23
  label: string;
20
24
  }
21
- /** Who a role can be granted to. */
22
- export interface AccessGroup {
25
+ /** Who a role can be granted to: a person, a group, or everyone. */
26
+ export interface AccessPrincipal {
23
27
  id: string;
24
28
  name: string;
25
- /** How many people are in it; for everyone, the organization's size. */
29
+ /** `everyone` is the whole organization, people who join later included. At most one
30
+ * principal is; leave it out where a resource can't be granted to everyone. */
31
+ kind: 'user' | 'group' | 'everyone';
32
+ /** For a group, how many people are in it; for everyone, the organization's size. */
26
33
  memberCount?: number | undefined;
27
- /** `everyone` is the whole organization, people who join later included. At most one group
28
- * is; leave it out where a resource can't be granted to everyone. */
29
- kind?: 'everyone' | 'group' | undefined;
34
+ /** For a person, their picture. Until it loads, their initials show. */
35
+ avatarUrl?: string | undefined;
36
+ /** For a person, what tells them apart from someone with the same name: an email address. */
37
+ detail?: string | undefined;
38
+ /** For a person, the groups they are in. Only groups holding a role here matter, so a host
39
+ * may pass just those; with them, taking a role from the person says what they keep through
40
+ * their groups. */
41
+ groupIds?: readonly string[] | undefined;
30
42
  }
31
- /** A role held by a group. */
43
+ /** A role held by a person or group. */
32
44
  export interface AccessGrant {
33
- groupId: string;
45
+ principalId: string;
34
46
  roleId: string;
35
47
  }
48
+ /** A role held through something the resource is part of, such as its organization. It shows,
49
+ * and counts toward what someone keeps, but changes only where it was granted. */
50
+ export interface AccessInheritedGrant extends AccessGrant {
51
+ /** Where it was granted, as a phrase that follows "through": "the organization". */
52
+ from: string;
53
+ }
36
54
  /** What one change grants and takes away; neither list holds a grant that changes nothing. */
37
55
  export interface AccessChange {
38
56
  grant: AccessGrant[];
@@ -43,7 +61,7 @@ export interface AccessMember {
43
61
  name: string;
44
62
  avatarUrl?: string | undefined;
45
63
  }
46
- /** More about a group than the list needs, loaded when it is highlighted in the group picker. */
64
+ /** More about a group than the list needs, loaded when it is highlighted in the picker. */
47
65
  export interface AccessGroupDetails {
48
66
  description?: string | undefined;
49
67
  /** `synced` comes from an identity provider; `manual` is kept by hand. */
@@ -1,7 +1,7 @@
1
1
  import { createMemoryConversationStore } from "../memoryStore.js";
2
2
  //#region src/ai/adapter/mock/mockAdapter.ts
3
3
  var CANNED_REPLIES = [
4
- "Here's a canned reply from the mock adapter. It streams word by word so the typewriter animation, cancel behavior, and background streams all exercise the same code paths as a real backend.\n\n- No network requests are made\n- Conversations persist in localStorage\n- Try cancelling mid-stream: the partial reply is kept",
4
+ "Here's a canned reply from the mock adapter. It streams word by word so the typewriter animation, cancel behavior, and background streams all exercise the same code paths as a real backend.\n\n- No network requests are made\n- Conversations persist in localStorage\n- Try canceling mid-stream: the partial reply is kept",
5
5
  "Everything you see is generated locally. The mock adapter implements the same `ChatAdapter` interface as the OpenAI-compatible adapter and any backend's own, so the UI can't tell the difference.\n\n```ts\nconst adapter = createMockChatAdapter()\n```\n\nMarkdown, `inline code`, and code blocks render through the same pipeline.",
6
6
  "Branching works too: edit one of your earlier messages or hit regenerate on a reply, then use the arrows to flip between branches."
7
7
  ];
@@ -26,7 +26,7 @@ interface UseSSEStreamResult {
26
26
  streamingConversationIdRef: React.MutableRefObject<string | null>;
27
27
  /** Per-conversation background stream storage */
28
28
  backgroundStreamsRef: React.MutableRefObject<Map<string, BackgroundStream>>;
29
- /** AbortController ref for cancelling fetch requests */
29
+ /** AbortController ref for canceling fetch requests */
30
30
  abortControllerRef: React.MutableRefObject<AbortController | null>;
31
31
  }
32
32
  export declare function useSSEStream(dispatch: React.Dispatch<ChatAction>): UseSSEStreamResult;
@@ -11,7 +11,7 @@ import { CheckboxGroup } from "@base-ui/react/checkbox-group";
11
11
  function Checkbox$1({ className, ...props }) {
12
12
  return /* @__PURE__ */ jsx(Checkbox.Root, {
13
13
  ...props,
14
- className: cx("flex size-4 shrink-0 items-center justify-center rounded border border-(--theme-border-strong) bg-(--theme-input-bg) text-(--theme-accent-text) cursor-pointer transition-colors motion-reduce:transition-none", focusOutline, "data-checked:border-(--theme-accent) data-checked:bg-(--theme-accent)", "data-indeterminate:border-(--theme-accent) data-indeterminate:bg-(--theme-accent)", "aria-invalid:border-(--theme-error)", "data-disabled:cursor-not-allowed data-disabled:opacity-50", className),
14
+ className: cx("flex size-4 shrink-0 items-center justify-center rounded border border-(--theme-border-strong) bg-(--theme-input-bg) text-(--theme-accent-text) cursor-pointer transition-colors motion-reduce:transition-none", focusOutline, "data-checked:border-(--theme-focus) data-checked:bg-(--theme-accent)", "data-indeterminate:border-(--theme-focus) data-indeterminate:bg-(--theme-accent)", "aria-invalid:border-(--theme-error)", "data-disabled:cursor-not-allowed data-disabled:opacity-50", className),
15
15
  children: /* @__PURE__ */ jsx(Checkbox.Indicator, {
16
16
  className: "flex",
17
17
  render: (indicatorProps, state) => /* @__PURE__ */ jsx("span", {
@@ -1,6 +1,7 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { AddIcon, CheckIcon, ChevronDownIcon, RemoveIcon } from "../icons.js";
3
3
  import { panelClassName, positionerClassName } from "./floating.js";
4
+ import { useFullscreenContainer } from "./fullscreen.js";
4
5
  import { Loader } from "./Loader.js";
5
6
  import { Tooltip } from "./Tooltip.js";
6
7
  import { useStrings } from "./Provider.js";
@@ -361,6 +362,7 @@ function GroupHeading({ group }) {
361
362
  }
362
363
  /** Portal, positioner and popup shared by Combobox and Autocomplete; both use the same parts. */
363
364
  function ListPopup({ listRef, empty, statusGroups = [], query = "", header, className = "w-(--anchor-width) max-w-(--available-width)", loading, details, collisionBoundary, renderItem }) {
365
+ const fullscreen = useFullscreenContainer();
364
366
  const withDetails = details !== void 0;
365
367
  const list = /* @__PURE__ */ jsxs(Fragment, { children: [
366
368
  loading && /* @__PURE__ */ jsxs("div", {
@@ -387,27 +389,30 @@ function ListPopup({ listRef, empty, statusGroups = [], query = "", header, clas
387
389
  })
388
390
  })
389
391
  ] });
390
- return /* @__PURE__ */ jsx(Combobox.Portal, { children: /* @__PURE__ */ jsx(Combobox.Positioner, {
391
- align: "start",
392
- sideOffset: 4,
393
- className: positionerClassName,
394
- ...collisionBoundary && { collisionBoundary },
395
- children: /* @__PURE__ */ jsxs(Combobox.Popup, {
396
- className: cx("overflow-hidden", withDetails && "@container/picker flex h-[min(40rem,var(--available-height))] flex-col", className, panelClassName),
397
- children: [header, withDetails ? /* @__PURE__ */ jsxs("div", {
398
- className: "flex min-h-0 flex-1",
399
- children: [/* @__PURE__ */ jsx("div", {
400
- className: "flex min-w-0 flex-1 flex-col",
401
- children: list
402
- }), /* @__PURE__ */ jsxs("div", {
403
- "aria-hidden": "true",
404
- "data-testid": "combobox-details",
405
- className: "hidden w-1/2 shrink-0 overflow-y-auto overscroll-contain border-l border-(--theme-border) bg-(--theme-panel-bg) @min-[30rem]/picker:block",
406
- children: [details, /* @__PURE__ */ jsx("div", { className: "pointer-events-none sticky bottom-0 -mt-6 h-6 bg-linear-to-t from-(--theme-panel-bg) to-transparent" })]
407
- })]
408
- }) : list]
392
+ return /* @__PURE__ */ jsx(Combobox.Portal, {
393
+ container: fullscreen,
394
+ children: /* @__PURE__ */ jsx(Combobox.Positioner, {
395
+ align: "start",
396
+ sideOffset: 4,
397
+ className: positionerClassName,
398
+ ...collisionBoundary && { collisionBoundary },
399
+ children: /* @__PURE__ */ jsxs(Combobox.Popup, {
400
+ className: cx("overflow-hidden", withDetails && "@container/picker flex h-[min(40rem,var(--available-height))] flex-col", className, panelClassName, !empty && !loading && statusGroups.length === 0 && !header && "data-empty:hidden"),
401
+ children: [header, withDetails ? /* @__PURE__ */ jsxs("div", {
402
+ className: "flex min-h-0 flex-1",
403
+ children: [/* @__PURE__ */ jsx("div", {
404
+ className: "flex min-w-0 flex-1 flex-col",
405
+ children: list
406
+ }), /* @__PURE__ */ jsxs("div", {
407
+ "aria-hidden": "true",
408
+ "data-testid": "combobox-details",
409
+ className: "hidden w-1/2 shrink-0 overflow-y-auto overscroll-contain border-l border-(--theme-border) bg-(--theme-panel-bg) @min-[30rem]/picker:block",
410
+ children: [details, /* @__PURE__ */ jsx("div", { className: "pointer-events-none sticky bottom-0 -mt-6 h-6 bg-linear-to-t from-(--theme-panel-bg) to-transparent" })]
411
+ })]
412
+ }) : list]
413
+ })
409
414
  })
410
- }) });
415
+ });
411
416
  }
412
417
  function toRows(filtered, statusGroups) {
413
418
  const nodes = statusGroups.length ? [...filtered, ...statusGroups].sort((a, b) => ("order" in a ? a.order : 0) - ("order" in b ? b.order : 0)) : filtered;
@@ -2,9 +2,10 @@ import { cx } from "../cx.js";
2
2
  import { buttonClasses } from "./buttonClasses.js";
3
3
  import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "../icons.js";
4
4
  import { panelClassName, positionerClassName } from "./floating.js";
5
+ import { useFullscreenContainer } from "./fullscreen.js";
5
6
  import { useStrings } from "./Provider.js";
6
7
  import { fieldBox } from "./fieldBox.js";
7
- import { useFieldControl, useFieldLabelId } from "./Field.js";
8
+ import { useFieldLabelledControl } from "./Field.js";
8
9
  import { jsx, jsxs } from "react/jsx-runtime";
9
10
  import { useState } from "react";
10
11
  import { Popover } from "@base-ui/react/popover";
@@ -74,10 +75,10 @@ var triggerClassName = (size) => cx(fieldBox({ size }), "flex flex-1 cursor-poin
74
75
  * and the shown date follow the reader's locale.
75
76
  */
76
77
  function DatePicker({ value, onValueChange, withTime = false, min, max, timeStep = 60, required = false, disabled = false, placeholder, id, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, "aria-required": ariaRequired, size = "md", className }) {
78
+ const fullscreen = useFullscreenContainer();
77
79
  const t = useStrings().datePicker;
78
80
  const { common } = useStrings();
79
- const field = useFieldControl();
80
- const fieldLabelId = useFieldLabelId();
81
+ const field = useFieldLabelledControl();
81
82
  const [open, setOpen] = useState(false);
82
83
  const [weekStartsOn] = useState(firstWeekday);
83
84
  const day = parseDay(value);
@@ -104,10 +105,10 @@ function DatePicker({ value, onValueChange, withTime = false, min, max, timeStep
104
105
  open,
105
106
  onOpenChange: setOpen,
106
107
  children: [/* @__PURE__ */ jsxs(Popover.Trigger, {
107
- id,
108
+ id: id ?? field?.id,
108
109
  disabled,
109
110
  "aria-label": ariaLabel,
110
- "aria-labelledby": ariaLabelledBy ?? (ariaLabel ? void 0 : fieldLabelId),
111
+ "aria-labelledby": ariaLabelledBy ?? (ariaLabel ? void 0 : field?.["aria-labelledby"]),
111
112
  "aria-describedby": ariaDescribedBy ?? field?.["aria-describedby"],
112
113
  "aria-invalid": ariaInvalid || field?.["aria-invalid"],
113
114
  "aria-required": ariaRequired || field?.["aria-required"],
@@ -119,61 +120,64 @@ function DatePicker({ value, onValueChange, withTime = false, min, max, timeStep
119
120
  className: cx("truncate", !day && "text-(--theme-placeholder)"),
120
121
  children: day ? shownDay.format(day) : placeholder ?? t.pickDate
121
122
  })]
122
- }), /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
123
- align: "start",
124
- sideOffset: 4,
125
- className: positionerClassName,
126
- children: /* @__PURE__ */ jsxs(Popover.Popup, {
127
- className: panelClassName,
128
- children: [/* @__PURE__ */ jsx(DayPicker, {
129
- ...required ? {
130
- mode: "single",
131
- required: true,
132
- selected: day,
133
- onSelect: pick
134
- } : {
135
- mode: "single",
136
- selected: day,
137
- onSelect: pick
138
- },
139
- ...opensOn && { defaultMonth: opensOn },
140
- ...earliest && { startMonth: earliest },
141
- ...latest && { endMonth: latest },
142
- disabled: outOfRange,
143
- weekStartsOn,
144
- autoFocus: true,
145
- showOutsideDays: true,
146
- fixedWeeks: true,
147
- classNames: calendarClassNames,
148
- formatters: {
149
- formatCaption: (month) => caption.format(month),
150
- formatWeekdayName: (date) => weekday.format(date)
151
- },
152
- labels: {
153
- labelDayButton: (date) => longDay.format(date),
154
- labelPrevious: () => t.previousMonth,
155
- labelNext: () => t.nextMonth,
156
- labelGrid: (month) => caption.format(month),
157
- labelWeekday: (date) => longWeekday.format(date)
158
- },
159
- components: { Chevron: ({ orientation }) => orientation === "left" ? /* @__PURE__ */ jsx(ChevronLeftIcon, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRightIcon, { "aria-hidden": "true" }) }
160
- }), !required && day && /* @__PURE__ */ jsx("div", {
161
- className: "flex justify-end border-t border-(--theme-border) px-3 py-2",
162
- children: /* @__PURE__ */ jsx("button", {
163
- type: "button",
164
- className: buttonClasses({
165
- variant: "ghost",
166
- size: "xs"
167
- }),
168
- onClick: () => {
169
- emit(void 0);
170
- setOpen(false);
123
+ }), /* @__PURE__ */ jsx(Popover.Portal, {
124
+ container: fullscreen,
125
+ children: /* @__PURE__ */ jsx(Popover.Positioner, {
126
+ align: "start",
127
+ sideOffset: 4,
128
+ className: positionerClassName,
129
+ children: /* @__PURE__ */ jsxs(Popover.Popup, {
130
+ className: panelClassName,
131
+ children: [/* @__PURE__ */ jsx(DayPicker, {
132
+ ...required ? {
133
+ mode: "single",
134
+ required: true,
135
+ selected: day,
136
+ onSelect: pick
137
+ } : {
138
+ mode: "single",
139
+ selected: day,
140
+ onSelect: pick
171
141
  },
172
- children: common.clear
173
- })
174
- })]
142
+ ...opensOn && { defaultMonth: opensOn },
143
+ ...earliest && { startMonth: earliest },
144
+ ...latest && { endMonth: latest },
145
+ disabled: outOfRange,
146
+ weekStartsOn,
147
+ autoFocus: true,
148
+ showOutsideDays: true,
149
+ fixedWeeks: true,
150
+ classNames: calendarClassNames,
151
+ formatters: {
152
+ formatCaption: (month) => caption.format(month),
153
+ formatWeekdayName: (date) => weekday.format(date)
154
+ },
155
+ labels: {
156
+ labelDayButton: (date) => longDay.format(date),
157
+ labelPrevious: () => t.previousMonth,
158
+ labelNext: () => t.nextMonth,
159
+ labelGrid: (month) => caption.format(month),
160
+ labelWeekday: (date) => longWeekday.format(date)
161
+ },
162
+ components: { Chevron: ({ orientation }) => orientation === "left" ? /* @__PURE__ */ jsx(ChevronLeftIcon, { "aria-hidden": "true" }) : /* @__PURE__ */ jsx(ChevronRightIcon, { "aria-hidden": "true" }) }
163
+ }), !required && day && /* @__PURE__ */ jsx("div", {
164
+ className: "flex justify-end border-t border-(--theme-border) px-3 py-2",
165
+ children: /* @__PURE__ */ jsx("button", {
166
+ type: "button",
167
+ className: buttonClasses({
168
+ variant: "ghost",
169
+ size: "xs"
170
+ }),
171
+ onClick: () => {
172
+ emit(void 0);
173
+ setOpen(false);
174
+ },
175
+ children: common.clear
176
+ })
177
+ })]
178
+ })
175
179
  })
176
- }) })]
180
+ })]
177
181
  }), withTime && /* @__PURE__ */ jsx("input", {
178
182
  type: "time",
179
183
  "aria-label": t.time,
@@ -1,5 +1,6 @@
1
1
  import { cx } from "../cx.js";
2
2
  import { ArrowLeftIcon, CloseIcon } from "../icons.js";
3
+ import { useFullscreenContainer } from "./fullscreen.js";
3
4
  import { useStrings } from "./Provider.js";
4
5
  import { IconButton } from "./IconButton.js";
5
6
  import { closeHandler } from "./dismiss.js";
@@ -16,6 +17,7 @@ import { Drawer } from "@base-ui/react/drawer";
16
17
  * `--app-inset-start` (a start-side sidebar's width, at least 2.5rem). Set both
17
18
  * on the document root. */
18
19
  function Drawer$1({ open, onClose, title, description, onBack, backLabel, footer, side = "right", width = 480, resizable = false, minWidth = 320, dismissible = true, modal = true, closeButton = true, contained = false, keepMounted = false, padded = true, className, children, ...popupProps }) {
20
+ const fullscreen = useFullscreenContainer();
19
21
  const { common } = useStrings();
20
22
  const container = useRef(null);
21
23
  const viewport = useRef(null);
@@ -42,7 +44,7 @@ function Drawer$1({ open, onClose, title, description, onBack, backLabel, footer
42
44
  ref: container,
43
45
  className: "contents"
44
46
  }), /* @__PURE__ */ jsxs(Drawer.Portal, {
45
- ...contained ? { container } : {},
47
+ container: contained ? container : fullscreen,
46
48
  keepMounted,
47
49
  className: contained ? "contents" : void 0,
48
50
  children: [modal !== false && /* @__PURE__ */ jsx(Drawer.Backdrop, { className: cx(area, "z-(--layer-drawer) bg-black/40 opacity-[calc(1-var(--drawer-swipe-progress))] transition-opacity duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-swiping:duration-0 motion-reduce:transition-none") }), /* @__PURE__ */ jsx(Drawer.Viewport, {