@crisp-ui-kit/crisp 0.57.2 → 0.57.4
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/dist/assigneepicker.css +13 -0
- package/dist/assigneepicker.d.ts +7 -0
- package/dist/assigneepicker.layered.css +15 -0
- package/dist/button.css +3 -0
- package/dist/button.d.ts +2 -2
- package/dist/button.layered.css +3 -0
- package/dist/chip.css +1 -1
- package/dist/chip.layered.css +1 -1
- package/dist/cjs/components/breadcrumb/breadcrumb.js +1 -1
- package/dist/cjs/components/button/button.recipe.js +33 -0
- package/dist/cjs/components/chip/chip.recipe.js +3 -1
- package/dist/cjs/components/datatable/datatable.js +6 -3
- package/dist/cjs/components/datatable/datatable.recipe.js +45 -6
- package/dist/cjs/components/datepickerfield/datepickerfield.js +2 -2
- package/dist/cjs/components/datepickerfield/datepickerfield.recipe.js +16 -3
- package/dist/cjs/components/menu/menu.js +65 -9
- package/dist/cjs/components/multiselect/multiselect.js +3 -0
- package/dist/cjs/components/multiselect/multiselect.recipe.js +18 -0
- package/dist/cjs/components/select/select.js +2 -2
- package/dist/cjs/components/select/select.recipe.js +19 -0
- package/dist/cjs/components/table/table.recipe.js +7 -1
- package/dist/cjs/core/floating-portal.js +7 -4
- package/dist/cjs/core/index.js +2 -1
- package/dist/cjs/core/place-floating.js +38 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/product/assigneepicker/assigneepicker.js +127 -0
- package/dist/cjs/product/assigneepicker/assigneepicker.recipe.js +36 -0
- package/dist/cjs/product/assigneepicker/index.js +7 -0
- package/dist/cjs/product/inbox/inbox.js +11 -3
- package/dist/cjs/product/notesgallery/index.js +7 -0
- package/dist/cjs/product/notesgallery/notesgallery.js +80 -0
- package/dist/cjs/product/notesgallery/notesgallery.recipe.js +46 -0
- package/dist/cjs/product/objectpagemenu/index.js +17 -0
- package/dist/cjs/product/objectpagemenu/objectpagemenu.js +26 -0
- package/dist/cjs/product/objectpagemenu/objectpagemenu.recipe.js +30 -0
- package/dist/cjs/product/objectsettings/objectsettings.js +107 -14
- package/dist/cjs/product/recordpage/index.js +2 -1
- package/dist/cjs/product/recordpage/recordpage.js +112 -7
- package/dist/cjs/product/recordpage/recordpage.recipe.js +63 -0
- package/dist/cjs/product/recordpicker/index.js +7 -0
- package/dist/cjs/product/recordpicker/recordpicker.js +100 -0
- package/dist/cjs/product/recordpicker/recordpicker.recipe.js +36 -0
- package/dist/cjs/product/savedviews/index.js +2 -1
- package/dist/cjs/product/savedviews/savedviews.js +22 -0
- package/dist/cjs/product/savedviews/savedviews.recipe.js +47 -0
- package/dist/cjs/product/share/index.js +7 -0
- package/dist/cjs/product/share/share.js +179 -0
- package/dist/cjs/product/share/share.recipe.js +114 -0
- package/dist/cjs/product/shortcutssheet/index.js +17 -0
- package/dist/cjs/product/shortcutssheet/shortcutssheet.js +125 -0
- package/dist/cjs/product/shortcutssheet/shortcutssheet.recipe.js +50 -0
- package/dist/cjs/product/taskeditpopover/index.js +7 -0
- package/dist/cjs/product/taskeditpopover/taskeditpopover.js +159 -0
- package/dist/cjs/product/taskeditpopover/taskeditpopover.recipe.js +68 -0
- package/dist/cjs/product/workspaceswitchermenu/index.js +17 -0
- package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.js +38 -0
- package/dist/cjs/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +34 -0
- package/dist/cjs/tokens/palette.js +27 -0
- package/dist/cjs/tokens/semantic.js +20 -0
- package/dist/datatable.css +50 -11
- package/dist/datatable.layered.css +50 -11
- package/dist/datepickerfield.css +8 -2
- package/dist/datepickerfield.d.ts +2 -2
- package/dist/datepickerfield.layered.css +8 -2
- package/dist/esm/components/breadcrumb/breadcrumb.js +1 -1
- package/dist/esm/components/button/button.recipe.js +33 -0
- package/dist/esm/components/chip/chip.recipe.js +3 -1
- package/dist/esm/components/datatable/datatable.js +6 -3
- package/dist/esm/components/datatable/datatable.recipe.js +45 -6
- package/dist/esm/components/datepickerfield/datepickerfield.js +2 -2
- package/dist/esm/components/datepickerfield/datepickerfield.recipe.js +16 -3
- package/dist/esm/components/menu/menu.js +64 -8
- package/dist/esm/components/multiselect/multiselect.js +3 -0
- package/dist/esm/components/multiselect/multiselect.recipe.js +18 -0
- package/dist/esm/components/select/select.js +2 -2
- package/dist/esm/components/select/select.recipe.js +19 -0
- package/dist/esm/components/table/table.recipe.js +7 -1
- package/dist/esm/core/floating-portal.js +7 -4
- package/dist/esm/core/index.js +1 -1
- package/dist/esm/core/place-floating.js +37 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/product/assigneepicker/assigneepicker.js +91 -0
- package/dist/esm/product/assigneepicker/assigneepicker.recipe.js +33 -0
- package/dist/esm/product/assigneepicker/index.js +2 -0
- package/dist/esm/product/inbox/inbox.js +11 -3
- package/dist/esm/product/notesgallery/index.js +2 -0
- package/dist/esm/product/notesgallery/notesgallery.js +44 -0
- package/dist/esm/product/notesgallery/notesgallery.recipe.js +43 -0
- package/dist/esm/product/objectpagemenu/index.js +1 -0
- package/dist/esm/product/objectpagemenu/objectpagemenu.js +23 -0
- package/dist/esm/product/objectpagemenu/objectpagemenu.recipe.js +27 -0
- package/dist/esm/product/objectsettings/objectsettings.js +106 -13
- package/dist/esm/product/recordpage/index.js +1 -1
- package/dist/esm/product/recordpage/recordpage.js +112 -8
- package/dist/esm/product/recordpage/recordpage.recipe.js +63 -0
- package/dist/esm/product/recordpicker/index.js +2 -0
- package/dist/esm/product/recordpicker/recordpicker.js +64 -0
- package/dist/esm/product/recordpicker/recordpicker.recipe.js +33 -0
- package/dist/esm/product/savedviews/index.js +1 -1
- package/dist/esm/product/savedviews/savedviews.js +22 -1
- package/dist/esm/product/savedviews/savedviews.recipe.js +47 -0
- package/dist/esm/product/share/index.js +2 -0
- package/dist/esm/product/share/share.js +143 -0
- package/dist/esm/product/share/share.recipe.js +111 -0
- package/dist/esm/product/shortcutssheet/index.js +1 -0
- package/dist/esm/product/shortcutssheet/shortcutssheet.js +88 -0
- package/dist/esm/product/shortcutssheet/shortcutssheet.recipe.js +47 -0
- package/dist/esm/product/taskeditpopover/index.js +2 -0
- package/dist/esm/product/taskeditpopover/taskeditpopover.js +123 -0
- package/dist/esm/product/taskeditpopover/taskeditpopover.recipe.js +65 -0
- package/dist/esm/product/workspaceswitchermenu/index.js +1 -0
- package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.js +35 -0
- package/dist/esm/product/workspaceswitchermenu/workspaceswitchermenu.recipe.js +31 -0
- package/dist/esm/tokens/palette.js +27 -0
- package/dist/esm/tokens/semantic.js +20 -0
- package/dist/multiselect.css +18 -0
- package/dist/multiselect.layered.css +18 -0
- package/dist/native/tokens.d.ts +1 -1
- package/dist/native/tokens.js +56 -4
- package/dist/notesgallery.css +15 -0
- package/dist/notesgallery.d.ts +7 -0
- package/dist/notesgallery.layered.css +17 -0
- package/dist/objectpagemenu.css +3 -0
- package/dist/objectpagemenu.d.ts +7 -0
- package/dist/objectpagemenu.layered.css +5 -0
- package/dist/recordpage.css +59 -0
- package/dist/recordpage.layered.css +59 -0
- package/dist/recordpicker.css +13 -0
- package/dist/recordpicker.d.ts +7 -0
- package/dist/recordpicker.layered.css +15 -0
- package/dist/savedviews.css +47 -0
- package/dist/savedviews.layered.css +47 -0
- package/dist/select.css +19 -0
- package/dist/select.layered.css +19 -0
- package/dist/share.css +84 -0
- package/dist/share.d.ts +7 -0
- package/dist/share.layered.css +86 -0
- package/dist/shortcutssheet.css +24 -0
- package/dist/shortcutssheet.d.ts +7 -0
- package/dist/shortcutssheet.layered.css +26 -0
- package/dist/styles.css +438 -16
- package/dist/styles.layered.css +438 -16
- package/dist/table.css +7 -2
- package/dist/table.layered.css +7 -2
- package/dist/taskeditpopover.css +36 -0
- package/dist/taskeditpopover.d.ts +7 -0
- package/dist/taskeditpopover.layered.css +38 -0
- package/dist/tokens.css +26 -0
- package/dist/tokens.json +56 -4
- package/dist/types/components/breadcrumb/breadcrumb.d.ts +9 -1
- package/dist/types/components/button/button.d.ts +2 -0
- package/dist/types/components/button/button.recipe.d.ts +12 -0
- package/dist/types/components/chip/chip.recipe.d.ts +1 -1
- package/dist/types/components/datatable/datatable.d.ts +8 -0
- package/dist/types/components/datepickerfield/datepickerfield.d.ts +4 -1
- package/dist/types/components/datepickerfield/datepickerfield.recipe.d.ts +18 -3
- package/dist/types/components/menu/menu.d.ts +50 -7
- package/dist/types/components/multiselect/multiselect.d.ts +9 -0
- package/dist/types/components/select/select.d.ts +10 -0
- package/dist/types/core/floating-portal.d.ts +6 -5
- package/dist/types/core/index.d.ts +1 -1
- package/dist/types/core/place-floating.d.ts +37 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/product/assigneepicker/assigneepicker.d.ts +36 -0
- package/dist/types/product/assigneepicker/assigneepicker.recipe.d.ts +3 -0
- package/dist/types/product/assigneepicker/index.d.ts +2 -0
- package/dist/types/product/inbox/inbox.d.ts +13 -1
- package/dist/types/product/notesgallery/index.d.ts +2 -0
- package/dist/types/product/notesgallery/notesgallery.d.ts +54 -0
- package/dist/types/product/notesgallery/notesgallery.recipe.d.ts +3 -0
- package/dist/types/product/objectpagemenu/index.d.ts +1 -0
- package/dist/types/product/objectpagemenu/objectpagemenu.d.ts +40 -0
- package/dist/types/product/objectpagemenu/objectpagemenu.recipe.d.ts +17 -0
- package/dist/types/product/objectsettings/objectsettings.d.ts +71 -15
- package/dist/types/product/recordpage/index.d.ts +1 -1
- package/dist/types/product/recordpage/recordpage.d.ts +142 -1
- package/dist/types/product/recordpicker/index.d.ts +2 -0
- package/dist/types/product/recordpicker/recordpicker.d.ts +38 -0
- package/dist/types/product/recordpicker/recordpicker.recipe.d.ts +3 -0
- package/dist/types/product/savedviews/index.d.ts +1 -1
- package/dist/types/product/savedviews/savedviews.d.ts +36 -0
- package/dist/types/product/share/index.d.ts +2 -0
- package/dist/types/product/share/share.d.ts +85 -0
- package/dist/types/product/share/share.recipe.d.ts +3 -0
- package/dist/types/product/shortcutssheet/index.d.ts +1 -0
- package/dist/types/product/shortcutssheet/shortcutssheet.d.ts +49 -0
- package/dist/types/product/shortcutssheet/shortcutssheet.recipe.d.ts +3 -0
- package/dist/types/product/taskeditpopover/index.d.ts +2 -0
- package/dist/types/product/taskeditpopover/taskeditpopover.d.ts +67 -0
- package/dist/types/product/taskeditpopover/taskeditpopover.recipe.d.ts +3 -0
- package/dist/types/product/workspaceswitchermenu/index.d.ts +1 -0
- package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.d.ts +54 -0
- package/dist/types/product/workspaceswitchermenu/workspaceswitchermenu.recipe.d.ts +19 -0
- package/dist/types/tokens/contract.d.ts +13 -0
- package/dist/types/tokens/palette.d.ts +52 -0
- package/dist/types/tokens/semantic.d.ts +20 -0
- package/dist/workspaceswitchermenu.css +4 -0
- package/dist/workspaceswitchermenu.d.ts +7 -0
- package/dist/workspaceswitchermenu.layered.css +6 -0
- package/package.json +41 -1
|
@@ -124,6 +124,19 @@ const config = defineRecipe({
|
|
|
124
124
|
the component's own comment) -- NOT \`display: contents\`, which
|
|
125
125
|
drops a button's accessible semantics in some engines. */
|
|
126
126
|
& .crisp-recordpage-edit-trigger { display: inline-flex; flex-wrap: wrap; align-items: center; gap: ${c.space["1"]}; margin: 0; padding: 0; border: none; background: none; text-align: start; font: inherit; color: inherit; cursor: pointer; }
|
|
127
|
+
/* #990: measured live on app.attio.com's URL/domain attribute (hover the
|
|
128
|
+
row) -- the link itself never carries the edit affordance, a
|
|
129
|
+
separate 20px button trails it, mounting only on hover: 16px icon,
|
|
130
|
+
rgba(0,0,0,0.55) (crisp's own color-mix(fgPrimary 55%) -- the exact
|
|
131
|
+
formula .crisp-datatable-entity-action's identical muted icon
|
|
132
|
+
already uses), 6px radius (radius.sm). Reveal is opacity-based,
|
|
133
|
+
not the mount/unmount Attio's own DOM does, so a keyboard user
|
|
134
|
+
tabbing to it (:focus-visible) doesn't need the row hovered first --
|
|
135
|
+
same mechanism DataTable's own row actions (#601) already use. */
|
|
136
|
+
& .crisp-recordpage-edit-affordance { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 20px; height: 20px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.sm}; background: none; font-family: inherit; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); cursor: pointer; opacity: 0; transition: opacity .12s, background-color .12s, color .12s; }
|
|
137
|
+
& .crisp-recordpage-field:hover .crisp-recordpage-edit-affordance,
|
|
138
|
+
& .crisp-recordpage-edit-affordance:focus-visible { opacity: 1; }
|
|
139
|
+
& .crisp-recordpage-edit-affordance:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
|
|
127
140
|
& .crisp-recordpage-empty { color: ${c.fgMuted}; }
|
|
128
141
|
|
|
129
142
|
/* #431: measured live on app.attio.com's record page Details column --
|
|
@@ -226,6 +239,31 @@ const config = defineRecipe({
|
|
|
226
239
|
& .crisp-recordpage-emptycard { display: flex; flex-direction: column; align-items: center; gap: ${c.space["1.5"]}; padding: ${c.space["6"]} ${c.space["3"]}; border: 1px dashed ${c.borderDefault}; border-radius: ${c.radius.xl}; color: ${c.fgMuted}; font-size: ${c.text.md}; text-align: center; }
|
|
227
240
|
& .crisp-recordpage-emptycard svg { color: ${c.fgMuted}; opacity: 0.7; }
|
|
228
241
|
|
|
242
|
+
/* #622: the Activity tab's own toolbar -- "Filters" + "+ Add meeting",
|
|
243
|
+
both the same measured button chrome as the topbar's own action
|
|
244
|
+
buttons (28px, e.field shadow, 14/500 label). */
|
|
245
|
+
& .crisp-recordpage-tabtoolbar { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; }
|
|
246
|
+
|
|
247
|
+
/* #622: Files tab's empty state -- measured live on Attio's own record
|
|
248
|
+
page (a company with no files uploaded). The 138x138 dotted-grid
|
|
249
|
+
backdrop is the same measured tile EmptyState's records variant uses
|
|
250
|
+
(crisp/#603), duplicated here rather than composed through that
|
|
251
|
+
component because this surface's title (16/600) and body (14/500,
|
|
252
|
+
63%-mixed fgPrimary, not the flat fgMuted EmptyState uses) measure to
|
|
253
|
+
their own values, matching neither of that component's existing
|
|
254
|
+
variants. */
|
|
255
|
+
& .crisp-recordpage-panel--center { flex: 1; align-items: center; justify-content: center; }
|
|
256
|
+
& .crisp-recordpage-illustrated { display: flex; flex-direction: column; align-items: center; gap: ${c.space["1.5"]}; text-align: center; }
|
|
257
|
+
& .crisp-recordpage-illustrated-icon { display: flex; align-items: center; justify-content: center; width: 138px; height: 138px; margin-bottom: ${c.space["2"]}; color: color-mix(in srgb, ${c.fgPrimary} 22%, transparent); background: radial-gradient(circle, color-mix(in srgb, ${c.fgPrimary} 12%, transparent) 1.2px, transparent 1.2px) 0 0 / 16px 16px; }
|
|
258
|
+
& .crisp-recordpage-illustrated-title { font-size: ${c.text.xl}; font-weight: ${c.weight.semibold}; color: ${c.fgPrimary}; }
|
|
259
|
+
/* #988: the Calls tab's own gated state measures a larger 20/24 title
|
|
260
|
+
than Files/Deals' 16/600 -- Attio's own "empty view" pattern (the
|
|
261
|
+
same 20/600 EmptyState's page/records variants already carry, not a
|
|
262
|
+
one-off). */
|
|
263
|
+
& .crisp-recordpage-illustrated-title--lg { font-size: 20px; line-height: 24px; }
|
|
264
|
+
& .crisp-recordpage-illustrated-desc { max-width: 320px; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
265
|
+
& .crisp-recordpage-illustrated-actions { display: inline-flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: ${c.space["1.5"]}; margin-top: ${c.space["2"]}; }
|
|
266
|
+
|
|
229
267
|
& .crisp-recordpage-action[data-active] svg { color: #f5b83d; fill: #f5b83d; }
|
|
230
268
|
|
|
231
269
|
& .crisp-recordstable-tag { display: inline-flex; align-items: center; height: ${c.size.chip}; padding: 0 ${c.space["1"]}; border-radius: ${c.radius.chip}; font-size: ${c.text.lg}; line-height: 20px; font-weight: ${c.weight.medium}; white-space: nowrap; background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: color-mix(in srgb, ${c.fgPrimary} 82%, transparent); }
|
|
@@ -236,6 +274,31 @@ const config = defineRecipe({
|
|
|
236
274
|
& .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, ${c.bgPurple} 16%, transparent); color: color-mix(in srgb, ${c.bgPurple} 80%, ${c.fgPrimary}); }
|
|
237
275
|
& .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, ${c.bgCyan} 18%, transparent); color: color-mix(in srgb, ${c.bgCyan} 82%, ${c.fgPrimary}); }
|
|
238
276
|
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, ${c.bgOrange} 16%, transparent); color: color-mix(in srgb, ${c.bgOrange} 82%, ${c.fgPrimary}); }
|
|
277
|
+
|
|
278
|
+
/* #620: the "Run workflow" quick action's own popover -- measured live:
|
|
279
|
+
a centered, top-anchored overlay sharing product/command's own
|
|
280
|
+
Cmd-K-palette shell exactly (elevation.modal, bgOverlayFrost 60% +
|
|
281
|
+
blur(15px), radius 2xl/xl outer/inner) -- duplicated here rather than
|
|
282
|
+
imported, the same "own copy per product surface" choice already
|
|
283
|
+
made elsewhere in this codebase (product/command sits outside this
|
|
284
|
+
lane's own product/recordpage ownership). */
|
|
285
|
+
& .crisp-recordpage-workflow-backdrop { position: fixed; inset: 0; z-index: 60; background: transparent; }
|
|
286
|
+
& .crisp-recordpage-workflow-positioner { position: fixed; inset: 0; z-index: 61; display: flex; align-items: flex-start; justify-content: center; padding: 15vh ${c.space["4"]} ${c.space["4"]}; }
|
|
287
|
+
& .crisp-recordpage-workflow-content { position: relative; display: flex; flex-direction: column; box-sizing: border-box; width: 90vw; max-width: 808px; padding: ${c.space["1"]}; border-radius: ${c.radius["2xl"]}; background: color-mix(in srgb, ${c.bgOverlayFrost} 60%, transparent); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); box-shadow: ${e.modal}; opacity: 1; transform: scale(1); transition: opacity .2s ease, transform .2s ease; @starting-style { opacity: 0; transform: scale(0.97); } }
|
|
288
|
+
& .crisp-recordpage-workflow-card { display: flex; flex-direction: column; box-sizing: border-box; border-radius: ${c.radius.xl}; background: ${c.bgSurface}; overflow: hidden; }
|
|
289
|
+
& .crisp-recordpage-workflow-header { display: flex; align-items: center; gap: ${c.space["2"]}; height: 52px; flex-shrink: 0; padding: 0 ${c.space["2"]} 0 ${c.space["4"]}; }
|
|
290
|
+
& .crisp-recordpage-workflow-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.07px; color: ${c.fgPrimary}; }
|
|
291
|
+
& .crisp-recordpage-workflow-close { display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; width: 28px; height: 28px; margin: 0; padding: 0; border: none; border-radius: ${c.radius.md}; background: none; color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s, color .12s; }
|
|
292
|
+
& .crisp-recordpage-workflow-close:hover { background: color-mix(in srgb, ${c.fgPrimary} 6%, transparent); color: ${c.fgPrimary}; }
|
|
293
|
+
& .crisp-recordpage-workflow-search { display: flex; align-items: center; min-height: 52px; padding: ${c.space["4"]}; }
|
|
294
|
+
& .crisp-recordpage-workflow-input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font-family: inherit; font-size: ${c.text.xl}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; }
|
|
295
|
+
& .crisp-recordpage-workflow-input::placeholder { color: ${c.fgMuted}; }
|
|
296
|
+
& .crisp-recordpage-workflow-sep { height: 1px; flex-shrink: 0; background: ${c.borderSubtle}; }
|
|
297
|
+
& .crisp-recordpage-workflow-empty { display: flex; flex-direction: column; align-items: center; gap: ${c.space["2"]}; padding: ${c.space["6"]} ${c.space["3"]}; text-align: center; }
|
|
298
|
+
& .crisp-recordpage-workflow-create { display: inline-flex; align-items: center; gap: ${c.space["1.5"]}; height: 32px; padding: 0 ${c.space["3"]}; margin-top: ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: ${c.bgSurface}; box-shadow: ${e.field}; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; cursor: pointer; transition: background-color .12s; }
|
|
299
|
+
& .crisp-recordpage-workflow-create:hover { background: ${c.bgRaised}; }
|
|
300
|
+
& .crisp-recordpage-workflow-footer { display: flex; align-items: center; height: 44px; flex-shrink: 0; padding: 0 ${c.space["2.5"]}; border-top: 1px solid ${c.borderSubtle}; }
|
|
301
|
+
& .crisp-recordpage-workflow-footer-nav { display: flex; align-items: center; gap: ${c.space["1.5"]}; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
239
302
|
`,
|
|
240
303
|
variants: {},
|
|
241
304
|
defaultVariants: {},
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Search } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { useListboxKeyboard } from "../../components/select/index.js";
|
|
5
|
+
import { Spinner } from "../../components/spinner/index.js";
|
|
6
|
+
import { useCrispMessage, useCrispMessages } from "../../core/index.js";
|
|
7
|
+
import { recordpickerRecipe } from "./recordpicker.recipe.js";
|
|
8
|
+
/**
|
|
9
|
+
* The record-search picker behind a task's "Add record" chip -- measured
|
|
10
|
+
* live on app.attio.com's Tasks page ("Edit task" -> "Add record"): a
|
|
11
|
+
* "Search..." row, then a FLAT list mixed across every object type (a
|
|
12
|
+
* person result sat first, ahead of several companies, with no section
|
|
13
|
+
* headers -- unlike the mention picker's own grouped Users/Records shape,
|
|
14
|
+
* crisp#646). Attio's own live search took a moment to resolve, showing a
|
|
15
|
+
* centred spinner + "Loading..." row meanwhile; not independently
|
|
16
|
+
* re-measured against a second empty-results case, so `emptyLabel`'s
|
|
17
|
+
* default copy ("No records") is inferred by symmetry with
|
|
18
|
+
* `AssigneePicker`'s own measured "No users", not itself pixel-measured.
|
|
19
|
+
*/
|
|
20
|
+
export function RecordPicker({ records, loading = false, onSelect, query: queryProp, onQueryChange, searchLabel: searchLabelProp, loadingLabel: loadingLabelProp, emptyLabel: emptyLabelProp, className, ...rest }) {
|
|
21
|
+
const searchLabel = useCrispMessage(searchLabelProp, "Search...");
|
|
22
|
+
const loadingLabel = useCrispMessage(loadingLabelProp, "Loading...");
|
|
23
|
+
// crisp/#471: `emptyLabel` is typed `React.ReactNode`, so it resolves by
|
|
24
|
+
// hand rather than through `useCrispMessage`, which is string-only --
|
|
25
|
+
// same reasoning as `Inbox`'s own `emptyTitle`/`emptySubtitle`.
|
|
26
|
+
const crispMessages = useCrispMessages();
|
|
27
|
+
const emptyLabel = emptyLabelProp ?? crispMessages["No records"] ?? "No records";
|
|
28
|
+
const [internalQuery, setInternalQuery] = React.useState("");
|
|
29
|
+
const query = queryProp ?? internalQuery;
|
|
30
|
+
const setQuery = (q) => {
|
|
31
|
+
if (queryProp === undefined)
|
|
32
|
+
setInternalQuery(q);
|
|
33
|
+
onQueryChange?.(q);
|
|
34
|
+
};
|
|
35
|
+
const [highlight, setHighlight] = React.useState(0);
|
|
36
|
+
const autoId = React.useId();
|
|
37
|
+
const listboxId = `${autoId}-listbox`;
|
|
38
|
+
const optionId = (i) => `${autoId}-opt-${i}`;
|
|
39
|
+
const select = (r) => {
|
|
40
|
+
onSelect?.(r);
|
|
41
|
+
};
|
|
42
|
+
const { onKeyDown } = useListboxKeyboard({
|
|
43
|
+
enabledIndexes: records.map((_, i) => i),
|
|
44
|
+
highlight,
|
|
45
|
+
setHighlight,
|
|
46
|
+
commit: () => {
|
|
47
|
+
const r = records[highlight];
|
|
48
|
+
if (r)
|
|
49
|
+
select(r);
|
|
50
|
+
},
|
|
51
|
+
close: () => { },
|
|
52
|
+
dismiss: () => { },
|
|
53
|
+
});
|
|
54
|
+
const activeId = !loading && highlight >= 0 && highlight < records.length
|
|
55
|
+
? optionId(highlight)
|
|
56
|
+
: undefined;
|
|
57
|
+
return (_jsxs("div", { className: recordpickerRecipe({ className }), ...rest, children: [_jsxs("div", { className: "crisp-recordpicker-search", children: [_jsx(Search, { size: 16, strokeWidth: 1.75 }), _jsx("input", { type: "text", className: "crisp-recordpicker-search-input", placeholder: searchLabel, value: query, onChange: (e) => {
|
|
58
|
+
setQuery(e.target.value);
|
|
59
|
+
setHighlight(0);
|
|
60
|
+
}, onKeyDown: onKeyDown, role: "combobox", "aria-expanded": "true", "aria-controls": listboxId, "aria-activedescendant": activeId,
|
|
61
|
+
// biome-ignore lint/a11y/noAutofocus: focus the search field the moment its enclosing popover opens
|
|
62
|
+
autoFocus: true })] }), loading ? (_jsxs("div", { className: "crisp-recordpicker-loading", children: [_jsx(Spinner, { size: 16 }), loadingLabel] })) : records.length === 0 ? (_jsx("div", { className: "crisp-recordpicker-empty", children: emptyLabel })) : (_jsx("div", { className: "crisp-recordpicker-list", role: "listbox", id: listboxId, children: records.map((r, i) => (_jsxs("button", { type: "button", id: optionId(i), role: "option", tabIndex: -1, "aria-selected": i === highlight, "data-highlighted": i === highlight ? "" : undefined, className: "crisp-recordpicker-option", onMouseEnter: () => setHighlight(i), onClick: () => select(r), children: [r.icon, r.name] }, r.id))) }))] }));
|
|
63
|
+
}
|
|
64
|
+
RecordPicker.displayName = "RecordPicker";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c } from "../../tokens/index.js";
|
|
3
|
+
// The record picker behind a task's "Add record" chip -- measured live on
|
|
4
|
+
// app.attio.com's Tasks page ("Edit task" -> "Add record" -> its own
|
|
5
|
+
// sub-popover, same measured 350px width as `AssigneePicker`'s): a
|
|
6
|
+
// borderless "Search..." row, then a flat list mixed across every object
|
|
7
|
+
// type (not grouped into sections -- that's the *mention* picker's own
|
|
8
|
+
// shape, crisp#646, a different, still-ungrouped-by-default surface here),
|
|
9
|
+
// each row a consumer-supplied type icon + the record's name. A "Loading..."
|
|
10
|
+
// row appears while results are in flight (the same measured centred
|
|
11
|
+
// Spinner + text DataTable's own #826 loading state already uses).
|
|
12
|
+
const config = defineRecipe({
|
|
13
|
+
name: "recordpicker",
|
|
14
|
+
base: `display: flex; flex-direction: column; box-sizing: border-box; width: 350px; font-family: ${c.font.sans};
|
|
15
|
+
|
|
16
|
+
& .crisp-recordpicker-search { display: flex; align-items: center; gap: ${c.space["2"]}; height: 36px; padding: 0 ${c.space["3"]}; color: ${c.fgMuted}; }
|
|
17
|
+
& .crisp-recordpicker-search svg { flex-shrink: 0; }
|
|
18
|
+
& .crisp-recordpicker-search-input { flex: 1; min-width: 0; border: none; outline: none; background: none; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; line-height: 20px; color: ${c.fgPrimary}; }
|
|
19
|
+
& .crisp-recordpicker-search-input::placeholder { color: ${c.fgMuted}; }
|
|
20
|
+
|
|
21
|
+
& .crisp-recordpicker-loading { display: flex; align-items: center; justify-content: center; gap: 10px; padding: ${c.space["8"]} ${c.space["3"]}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; line-height: 20px; }
|
|
22
|
+
& .crisp-recordpicker-empty { padding: ${c.space["4"]} ${c.space["3"]}; text-align: center; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: color-mix(in srgb, ${c.fgPrimary} 63%, transparent); }
|
|
23
|
+
|
|
24
|
+
& .crisp-recordpicker-list { display: flex; flex-direction: column; margin: 0; padding: ${c.space["1"]}; list-style: none; max-height: 280px; overflow-y: auto; }
|
|
25
|
+
& .crisp-recordpicker-option { display: flex; align-items: center; gap: ${c.space["2"]}; width: 100%; box-sizing: border-box; height: 32px; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md}; background: none; font-family: inherit; font-size: ${c.text.lg}; color: ${c.fgPrimary}; text-align: start; cursor: pointer; }
|
|
26
|
+
& .crisp-recordpicker-option svg { flex-shrink: 0; color: ${c.fgMuted}; }
|
|
27
|
+
& .crisp-recordpicker-option:hover, & .crisp-recordpicker-option[data-highlighted] { background: ${c.bgRaised}; }`,
|
|
28
|
+
variants: {},
|
|
29
|
+
defaultVariants: {},
|
|
30
|
+
compoundVariants: [],
|
|
31
|
+
});
|
|
32
|
+
export const recordpickerRecipe = resolveRecipe(config);
|
|
33
|
+
export default config;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { SavedViews, } from "./savedviews.js";
|
|
1
|
+
export { SavedViews, SavedViewsSaveBar, } from "./savedviews.js";
|
|
2
2
|
export { savedviewsRecipe } from "./savedviews.recipe.js";
|
|
@@ -3,7 +3,7 @@ import { ChevronDown, MoreVertical, Plus, Users2 } from "lucide-react";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Menu, MenuItem, MenuSeparator } from "../../components/menu/index.js";
|
|
5
5
|
import { useListboxKeyboard } from "../../components/select/index.js";
|
|
6
|
-
import { FloatingPortal, hasNestedFloatingLayer, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
|
|
6
|
+
import { cx, FloatingPortal, hasNestedFloatingLayer, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
|
|
7
7
|
import { savedviewsRecipe } from "./savedviews.recipe.js";
|
|
8
8
|
// #618: the same fallback Select's own `optionText` gives a non-string
|
|
9
9
|
// `label` — `keywords` first, then the plain string itself, then (rather
|
|
@@ -200,3 +200,24 @@ export function SavedViews({ label, icon, views, activeId, onSelect, onCreate, o
|
|
|
200
200
|
}, children: [_jsx("span", { className: "crisp-savedviews-row-icon", "aria-hidden": "true", children: _jsx(Plus, { size: 16, strokeWidth: 1.75 }) }), _jsx("span", { className: "crisp-savedviews-row-label", children: createLabel })] })] }) }))] }));
|
|
201
201
|
}
|
|
202
202
|
SavedViews.displayName = "SavedViews";
|
|
203
|
+
/**
|
|
204
|
+
* The "unsaved changes" bar for a currently loaded saved view — Attio's own
|
|
205
|
+
* "Discard changes" + a split "Save for everyone ▾" button, measured live
|
|
206
|
+
* (app.attio.com, a saved Companies view with its sort changed). Not a part
|
|
207
|
+
* of `SavedViews`' own trigger/panel: Attio renders this in the page
|
|
208
|
+
* toolbar's own trailing slot, replacing whatever sits there at rest
|
|
209
|
+
* (Import/Export, "+ New …") — the same slot `TableToolbar`'s own
|
|
210
|
+
* `children` prop already documents ("Right-aligned actions after the
|
|
211
|
+
* count"). A consumer swaps this in for that slot's usual content once its
|
|
212
|
+
* own dirty check (live filters/sort/columns vs. the loaded view's
|
|
213
|
+
* snapshot) is true, and swaps it back out once it's false — this
|
|
214
|
+
* component itself carries no dirty state or visibility toggle.
|
|
215
|
+
*/
|
|
216
|
+
export function SavedViewsSaveBar({ onSave, onDiscard, onSaveAsNew, discardLabel: discardLabelProp, saveLabel: saveLabelProp, saveAsNewLabel: saveAsNewLabelProp, saveOptionsLabel: saveOptionsLabelProp, className, ...rest }) {
|
|
217
|
+
const discardLabel = useCrispMessage(discardLabelProp, "Discard changes");
|
|
218
|
+
const saveLabel = useCrispMessage(saveLabelProp, "Save for everyone");
|
|
219
|
+
const saveAsNewLabel = useCrispMessage(saveAsNewLabelProp, "Save as new view");
|
|
220
|
+
const saveOptionsLabel = useCrispMessage(saveOptionsLabelProp, "More save options");
|
|
221
|
+
return (_jsxs("div", { className: cx("crisp-savedviews-savebar", className), ...rest, children: [_jsx("button", { type: "button", className: "crisp-savedviews-savebar-discard", onClick: onDiscard, children: discardLabel }), _jsxs("div", { className: "crisp-savedviews-savebar-save-group", children: [_jsx("button", { type: "button", className: "crisp-savedviews-savebar-save", onClick: onSave, children: saveLabel }), onSaveAsNew && (_jsx(Menu, { align: "end", trigger: _jsx("button", { type: "button", className: "crisp-savedviews-savebar-save-chevron", "aria-label": saveOptionsLabel, children: _jsx(ChevronDown, { size: 16, strokeWidth: 1.75 }) }), children: _jsx(MenuItem, { onSelect: onSaveAsNew, children: saveAsNewLabel }) }))] })] }));
|
|
222
|
+
}
|
|
223
|
+
SavedViewsSaveBar.displayName = "SavedViewsSaveBar";
|
|
@@ -120,6 +120,53 @@ const config = defineRecipe({
|
|
|
120
120
|
font-size: ${c.text.sm}; font-weight: ${c.weight.medium};
|
|
121
121
|
color: color-mix(in srgb, ${c.fgPrimary} 50%, transparent);
|
|
122
122
|
}
|
|
123
|
+
|
|
124
|
+
/* crisp#568: the "Discard changes" / "Save for everyone ▾" bar a
|
|
125
|
+
consumer swaps into its own toolbar's trailing slot (TableToolbar's
|
|
126
|
+
own children prop already documents "right-aligned actions after the
|
|
127
|
+
count") while the loaded view is dirty -- measured live on
|
|
128
|
+
app.attio.com replacing that exact slot's usual Import/Export + New
|
|
129
|
+
button. Not rendered inside SavedViews' own trigger/panel, which
|
|
130
|
+
stays untouched; this is its own export, SavedViewsSaveBar. */
|
|
131
|
+
.crisp-savedviews-savebar { display: flex; align-items: center; gap: ${c.space["2"]}; }
|
|
132
|
+
/* Rest-state color is a 55% color-mix of fgPrimary -- measured
|
|
133
|
+
identical to recordpanel's own ".crisp-recordpanel-section-action"
|
|
134
|
+
(its own comment: "measured 10px/400, a lighter mix..." -- font size
|
|
135
|
+
there was the SAME outer-vs-inner-div miss #688 corrected; the 55%
|
|
136
|
+
mix itself checks out independently here). Hover repeats crisp's own
|
|
137
|
+
already-measured Button ghost intent exactly (bgRaised + fgPrimary),
|
|
138
|
+
confirmed live (hover bg rgba(0,0,0,.04) + ink text matches bgRaised
|
|
139
|
+
+ fgPrimary bit for bit) -- reused, not re-derived. */
|
|
140
|
+
.crisp-savedviews-savebar-discard {
|
|
141
|
+
box-sizing: border-box; display: inline-flex; align-items: center; height: 28px;
|
|
142
|
+
margin: 0; padding-inline: ${c.space["2"]}; border: none; border-radius: ${c.radius.md};
|
|
143
|
+
background: none; font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium};
|
|
144
|
+
color: color-mix(in srgb, ${c.fgPrimary} 55%, transparent); cursor: pointer;
|
|
145
|
+
transition: background-color .12s ease, color .12s ease;
|
|
146
|
+
}
|
|
147
|
+
.crisp-savedviews-savebar-discard:hover { background: ${c.bgRaised}; color: ${c.fgPrimary}; }
|
|
148
|
+
/* the split "Save for everyone ▾" pill -- two adjacent buttons sharing
|
|
149
|
+
one visual pill (measured: label half rounds only its outer/left
|
|
150
|
+
corners, the chevron half only its outer/right corners, seamless
|
|
151
|
+
between). Fill is crisp's OWN measured bgBrandContrast, not Attio's
|
|
152
|
+
own literal button blue -- the owner's directive is Attio structure/
|
|
153
|
+
sizing, seed-identity color (crisp direction doc). Hover/active reuse
|
|
154
|
+
Button's own already-measured primary-intent brightness filter
|
|
155
|
+
(crisp#910) rather than Attio's own literal darker fill, for the
|
|
156
|
+
same reason. */
|
|
157
|
+
.crisp-savedviews-savebar-save-group { display: flex; }
|
|
158
|
+
.crisp-savedviews-savebar-save, .crisp-savedviews-savebar-save-chevron {
|
|
159
|
+
box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
|
|
160
|
+
height: 28px; margin: 0; border: none; background: ${c.bgBrandContrast}; color: ${c.fgOnBrand};
|
|
161
|
+
cursor: pointer; transition: filter .2s;
|
|
162
|
+
}
|
|
163
|
+
.crisp-savedviews-savebar-save:hover, .crisp-savedviews-savebar-save-chevron:hover { filter: brightness(0.96); }
|
|
164
|
+
.crisp-savedviews-savebar-save:active, .crisp-savedviews-savebar-save-chevron:active { filter: brightness(0.92); }
|
|
165
|
+
.crisp-savedviews-savebar-save {
|
|
166
|
+
padding-inline: ${c.space["2"]} ${c.space["1.5"]}; border-radius: ${c.radius.md} 0 0 ${c.radius.md};
|
|
167
|
+
font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium};
|
|
168
|
+
}
|
|
169
|
+
.crisp-savedviews-savebar-save-chevron { width: 28px; padding: 0; border-radius: 0 ${c.radius.md} ${c.radius.md} 0; }
|
|
123
170
|
`,
|
|
124
171
|
variants: {},
|
|
125
172
|
defaultVariants: {},
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HelpCircle, Play, Users } from "lucide-react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Avatar } from "../../components/avatar/index.js";
|
|
5
|
+
import { Input } from "../../components/input/index.js";
|
|
6
|
+
import { Select } from "../../components/select/index.js";
|
|
7
|
+
import { useTablistKeyboard } from "../../components/tabs/index.js";
|
|
8
|
+
import { FloatingPortal, hasNestedFloatingLayer, isInsideLaterFloatingLayer, placeFloating, registerFloatingLayer, useCrispMessage, } from "../../core/index.js";
|
|
9
|
+
import { shareRecipe } from "./share.recipe.js";
|
|
10
|
+
/**
|
|
11
|
+
* A Share/permissions popover — measured live on app.attio.com (both an
|
|
12
|
+
* object table's and a List's own "Share" trigger, techpartners workspace):
|
|
13
|
+
* Members/Automations pill tabs, a "Workspace access" row (avatar + label +
|
|
14
|
+
* a `Read and write ⌄`-style role select), an optional plan-gated upsell
|
|
15
|
+
* slot, a "Clear permissions" action, an "Individual members" list (one row
|
|
16
|
+
* per member, its own role select), and — on the Automations tab — a
|
|
17
|
+
* search box + role select + "Add" row. Both tabs share a trailing "Learn
|
|
18
|
+
* more about permissions" link.
|
|
19
|
+
*
|
|
20
|
+
* Its own floating shell (not the generic `Popover`): the measured surface
|
|
21
|
+
* is 440px wide, past `Popover`'s 360px content cap. See share.recipe.ts.
|
|
22
|
+
*/
|
|
23
|
+
export function SharePopover({ trigger, align = "end", open: openProp, defaultOpen, onOpenChange, tab: tabProp, defaultTab = "members", onTabChange, membersTabLabel: membersTabLabelProp, automationsTabLabel: automationsTabLabelProp, workspaceIcon, workspaceAccessLabel: workspaceAccessLabelProp, workspaceAccessValue, workspaceAccessOptions, onWorkspaceAccessChange, advancedSharing, onClearPermissions, clearPermissionsLabel: clearPermissionsLabelProp, members, membersSectionLabel: membersSectionLabelProp, memberRoleOptions, onMemberAccessChange, automationSearchValue, defaultAutomationSearchValue, onAutomationSearchChange, automationSearchPlaceholder: automationSearchPlaceholderProp, automationRoleValue, automationRoleOptions, onAutomationRoleChange, onAddAutomation, addAutomationDisabled, addAutomationLabel: addAutomationLabelProp, learnMoreHref, learnMoreLabel: learnMoreLabelProp, "aria-label": ariaLabel, className, ...rest }) {
|
|
24
|
+
const membersTabLabel = useCrispMessage(membersTabLabelProp, "Members");
|
|
25
|
+
const automationsTabLabel = useCrispMessage(automationsTabLabelProp, "Automations");
|
|
26
|
+
const workspaceAccessLabel = useCrispMessage(workspaceAccessLabelProp, "Workspace access");
|
|
27
|
+
const clearPermissionsLabel = useCrispMessage(clearPermissionsLabelProp, "Clear permissions");
|
|
28
|
+
const membersSectionLabel = useCrispMessage(membersSectionLabelProp, "Individual members");
|
|
29
|
+
const automationSearchPlaceholder = useCrispMessage(automationSearchPlaceholderProp, "Search...");
|
|
30
|
+
const addAutomationLabel = useCrispMessage(addAutomationLabelProp, "Add");
|
|
31
|
+
const learnMoreLabel = useCrispMessage(learnMoreLabelProp, "Learn more about permissions");
|
|
32
|
+
const surfaceId = React.useId();
|
|
33
|
+
const triggerId = trigger.props.id ?? `${surfaceId}-trigger`;
|
|
34
|
+
const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
|
|
35
|
+
const open = openProp ?? internalOpen;
|
|
36
|
+
const setOpen = React.useCallback((next) => {
|
|
37
|
+
if (openProp === undefined)
|
|
38
|
+
setInternalOpen(next);
|
|
39
|
+
onOpenChange?.(next);
|
|
40
|
+
}, [openProp, onOpenChange]);
|
|
41
|
+
const [internalTab, setInternalTab] = React.useState(defaultTab);
|
|
42
|
+
const activeTab = tabProp ?? internalTab;
|
|
43
|
+
const selectTab = (t) => {
|
|
44
|
+
if (tabProp === undefined)
|
|
45
|
+
setInternalTab(t);
|
|
46
|
+
onTabChange?.(t);
|
|
47
|
+
};
|
|
48
|
+
const [internalSearch, setInternalSearch] = React.useState(defaultAutomationSearchValue ?? "");
|
|
49
|
+
const searchValue = automationSearchValue ?? internalSearch;
|
|
50
|
+
const setSearch = (q) => {
|
|
51
|
+
if (automationSearchValue === undefined)
|
|
52
|
+
setInternalSearch(q);
|
|
53
|
+
onAutomationSearchChange?.(q);
|
|
54
|
+
};
|
|
55
|
+
const rootRef = React.useRef(null);
|
|
56
|
+
const triggerRef = React.useRef(null);
|
|
57
|
+
const contentRef = React.useRef(null);
|
|
58
|
+
const [pos, setPos] = React.useState({});
|
|
59
|
+
const [up, setUp] = React.useState(false);
|
|
60
|
+
const place = () => {
|
|
61
|
+
const el = triggerRef.current;
|
|
62
|
+
if (!el)
|
|
63
|
+
return;
|
|
64
|
+
const { style, up: flipUp } = placeFloating({
|
|
65
|
+
rect: el.getBoundingClientRect(),
|
|
66
|
+
align,
|
|
67
|
+
gap: 6,
|
|
68
|
+
flipBelow: 320,
|
|
69
|
+
anchor: el,
|
|
70
|
+
});
|
|
71
|
+
setUp(flipUp);
|
|
72
|
+
setPos(style);
|
|
73
|
+
};
|
|
74
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: reposition each time it opens
|
|
75
|
+
React.useLayoutEffect(() => {
|
|
76
|
+
if (open)
|
|
77
|
+
place();
|
|
78
|
+
}, [open]);
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
if (!open || !contentRef.current)
|
|
81
|
+
return;
|
|
82
|
+
return registerFloatingLayer(contentRef.current);
|
|
83
|
+
}, [open]);
|
|
84
|
+
const close = React.useCallback(() => {
|
|
85
|
+
setOpen(false);
|
|
86
|
+
}, [setOpen]);
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
if (!open)
|
|
89
|
+
return;
|
|
90
|
+
const onDown = (e) => {
|
|
91
|
+
const target = e.target;
|
|
92
|
+
if (rootRef.current?.contains(target))
|
|
93
|
+
return;
|
|
94
|
+
if (contentRef.current?.contains(target))
|
|
95
|
+
return;
|
|
96
|
+
if (isInsideLaterFloatingLayer(target, contentRef.current))
|
|
97
|
+
return;
|
|
98
|
+
close();
|
|
99
|
+
};
|
|
100
|
+
document.addEventListener("pointerdown", onDown);
|
|
101
|
+
return () => document.removeEventListener("pointerdown", onDown);
|
|
102
|
+
}, [open, close]);
|
|
103
|
+
React.useEffect(() => {
|
|
104
|
+
if (!open)
|
|
105
|
+
return;
|
|
106
|
+
const onKey = (e) => {
|
|
107
|
+
if (e.key !== "Escape")
|
|
108
|
+
return;
|
|
109
|
+
if (hasNestedFloatingLayer(contentRef.current))
|
|
110
|
+
return;
|
|
111
|
+
close();
|
|
112
|
+
};
|
|
113
|
+
document.addEventListener("keydown", onKey);
|
|
114
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
115
|
+
}, [open, close]);
|
|
116
|
+
const triggerEl = React.cloneElement(trigger, {
|
|
117
|
+
ref: triggerRef,
|
|
118
|
+
id: triggerId,
|
|
119
|
+
"aria-haspopup": "dialog",
|
|
120
|
+
"aria-expanded": open,
|
|
121
|
+
"aria-controls": open ? surfaceId : undefined,
|
|
122
|
+
onClick: (e) => {
|
|
123
|
+
trigger.props.onClick?.(e);
|
|
124
|
+
setOpen(!open);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
const tablistRef = React.useRef(null);
|
|
128
|
+
const tabEntries = React.useMemo(() => [{ value: "members" }, { value: "automations" }], []);
|
|
129
|
+
const { onKeyDown: onTabKeyDown, tabIndexFor } = useTablistKeyboard({
|
|
130
|
+
tabs: tabEntries,
|
|
131
|
+
active: activeTab,
|
|
132
|
+
select: (v) => selectTab(v),
|
|
133
|
+
listRef: tablistRef,
|
|
134
|
+
});
|
|
135
|
+
return (_jsxs("div", { ref: rootRef, className: shareRecipe({ className }), children: [triggerEl, open && (_jsx(FloatingPortal, { sourceRef: rootRef, children: _jsxs("div", { ref: contentRef, className: "crisp-share-content", role: "dialog", id: surfaceId, "aria-label": ariaLabel, "aria-labelledby": ariaLabel ? undefined : triggerId, "data-up": up ? "" : undefined, style: pos, ...rest, children: [_jsxs("div", { ref: tablistRef, className: "crisp-share-tabs", role: "tablist", onKeyDown: onTabKeyDown, children: [_jsxs("button", { type: "button", role: "tab", "data-value": "members", "aria-selected": activeTab === "members", tabIndex: tabIndexFor("members"), className: "crisp-share-tab", onClick: () => selectTab("members"), children: [_jsx(Users, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), membersTabLabel] }), _jsxs("button", { type: "button", role: "tab", "data-value": "automations", "aria-selected": activeTab === "automations", tabIndex: tabIndexFor("automations"), className: "crisp-share-tab", onClick: () => selectTab("automations"), children: [_jsx(Play, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), automationsTabLabel] })] }), activeTab === "members" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "crisp-share-row", children: [workspaceIcon ?? (_jsx(Avatar, { size: "lg", tone: "brand", children: typeof workspaceAccessLabel === "string"
|
|
136
|
+
? workspaceAccessLabel.charAt(0)
|
|
137
|
+
: null })), _jsx("span", { className: "crisp-share-row-label", children: workspaceAccessLabel }), _jsx(Select, { variant: "ghost", width: "auto", options: workspaceAccessOptions, value: workspaceAccessValue, onValueChange: onWorkspaceAccessChange, "aria-label": workspaceAccessLabel })] }), advancedSharing, onClearPermissions && (_jsx("button", { type: "button", className: "crisp-share-clear", onClick: onClearPermissions, children: clearPermissionsLabel })), members && members.length > 0 && (_jsxs("div", { children: [_jsx("p", { className: "crisp-share-section-label", children: membersSectionLabel }), _jsx("div", { className: "crisp-share-members", children: members.map((m) => (_jsxs("div", { className: "crisp-share-member", children: [m.avatar ?? (_jsx(Avatar, { size: "lg", tone: "brand", children: typeof m.name === "string"
|
|
138
|
+
? m.name.charAt(0).toUpperCase()
|
|
139
|
+
: null })), _jsxs("span", { className: "crisp-share-member-text", children: [_jsx("span", { className: "crisp-share-member-name", children: m.name }), m.email && (_jsx("span", { className: "crisp-share-member-email", children: m.email }))] }), _jsx(Select, { variant: "ghost", width: "auto", options: memberRoleOptions, value: m.access, disabled: m.accessDisabled, onValueChange: (v) => onMemberAccessChange?.(m.id, v), "aria-label": typeof m.name === "string"
|
|
140
|
+
? `${m.name}'s access`
|
|
141
|
+
: undefined })] }, m.id))) })] }))] })) : (_jsxs("div", { className: "crisp-share-automations-row", children: [_jsx(Input, { value: searchValue, onChange: (e) => setSearch(e.target.value), placeholder: automationSearchPlaceholder, "aria-label": automationSearchPlaceholder }), _jsx(Select, { variant: "ghost", width: "auto", options: automationRoleOptions, value: automationRoleValue, onValueChange: onAutomationRoleChange, "aria-label": automationsTabLabel }), _jsx("button", { type: "button", className: "crisp-share-add", disabled: addAutomationDisabled, onClick: onAddAutomation, children: addAutomationLabel })] })), _jsxs("a", { className: "crisp-share-learnmore", href: learnMoreHref, target: "_blank", rel: "noreferrer", children: [_jsx(HelpCircle, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }), learnMoreLabel] })] }) }))] }));
|
|
142
|
+
}
|
|
143
|
+
SharePopover.displayName = "SharePopover";
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { defineRecipe, resolveRecipe } from "../../core/index.js";
|
|
2
|
+
import { contract as c, elevation as e } from "../../tokens/index.js";
|
|
3
|
+
// SharePopover's shell — measured live on app.attio.com (both the object
|
|
4
|
+
// table's and a List's own "Share" trigger, techpartners workspace,
|
|
5
|
+
// 2026-09-27): a 440px-wide floating panel, radius 12 (xl), the same
|
|
6
|
+
// layered popover shadow every other floating surface uses (`e.floating`
|
|
7
|
+
// matched the measured box-shadow's four layers exactly, so no new shadow
|
|
8
|
+
// literal was needed).
|
|
9
|
+
//
|
|
10
|
+
// This is its own floating shell rather than a reuse of the generic
|
|
11
|
+
// `Popover` (components/popover): that recipe caps its content at
|
|
12
|
+
// `min(360px, 92vw)`, and 440px measured wider than that on every sample.
|
|
13
|
+
// `product/savedviews` already sets the precedent for a product surface
|
|
14
|
+
// owning its own portaled shell over the generic one when the measured
|
|
15
|
+
// geometry doesn't fit it.
|
|
16
|
+
//
|
|
17
|
+
// Three muted-text alphas were measured that don't match any existing
|
|
18
|
+
// color token: the workspace-access/role value text (`rgba(0,0,0,.55)`),
|
|
19
|
+
// the "Individual members" section label (`rgba(0,0,0,.5)`), and the
|
|
20
|
+
// inactive-tab/footer-link text (`rgba(0,0,0,.63)` — this one incidentally
|
|
21
|
+
// equals `fgAlertDescription`'s light value, but that token is Dialog's own
|
|
22
|
+
// alert-description colour, a different surface, so it is not reused here).
|
|
23
|
+
// Rather than add new palette entries for three one-off alphas (tokens/**
|
|
24
|
+
// is another lane's file), each is expressed as `color-mix(in srgb,
|
|
25
|
+
// fgPrimary N%, transparent)` — the same idiom `segmented`'s own track
|
|
26
|
+
// background already uses for its 5% tint — so the value is theme-reactive
|
|
27
|
+
// (it scales with fgPrimary's dark value too) without a new token or a
|
|
28
|
+
// flat literal that would go stale in dark mode.
|
|
29
|
+
const mutedText = (pct) => `color-mix(in srgb, ${c.fgPrimary} ${pct}%, transparent)`;
|
|
30
|
+
const config = defineRecipe({
|
|
31
|
+
name: "share",
|
|
32
|
+
base: `display: contents; font-family: ${c.font.sans};`,
|
|
33
|
+
globalCss: `
|
|
34
|
+
.crisp-share-content {
|
|
35
|
+
font-family: ${c.font.sans}; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
|
|
36
|
+
position: fixed; z-index: 70; pointer-events: auto; box-sizing: border-box;
|
|
37
|
+
width: 440px; max-width: 92vw;
|
|
38
|
+
padding: ${c.space["3"]}; display: flex; flex-direction: column; gap: ${c.space["3"]};
|
|
39
|
+
border-radius: ${c.radius.xl}; background: ${c.bgPopover}; color: ${c.fgPrimary};
|
|
40
|
+
box-shadow: ${e.floating};
|
|
41
|
+
opacity: 1; transform: none; transform-origin: top right;
|
|
42
|
+
transition: opacity 0.08s ease, transform 0.08s ease;
|
|
43
|
+
@starting-style { opacity: 0; transform: scale(0.97) translateY(-1px); }
|
|
44
|
+
}
|
|
45
|
+
.crisp-share-content[data-up] { transform-origin: bottom right; }
|
|
46
|
+
|
|
47
|
+
.crisp-share-tabs { display: flex; align-items: center; gap: ${c.space["1"]}; }
|
|
48
|
+
.crisp-share-tab {
|
|
49
|
+
box-sizing: border-box; display: inline-flex; align-items: center; gap: ${c.space["1.5"]};
|
|
50
|
+
height: ${c.size.row}; padding: 0 ${c.space["2"]}; border: none; border-radius: ${c.radius.md};
|
|
51
|
+
background: transparent; color: ${mutedText(63)};
|
|
52
|
+
font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.07px;
|
|
53
|
+
cursor: pointer; transition: background-color .12s ease, color .12s ease;
|
|
54
|
+
}
|
|
55
|
+
.crisp-share-tab svg { flex-shrink: 0; }
|
|
56
|
+
.crisp-share-tab:hover { color: ${c.fgPrimary}; }
|
|
57
|
+
.crisp-share-tab[aria-selected="true"] { background: ${c.bgSurfaceMuted}; color: ${c.fgPrimary}; }
|
|
58
|
+
|
|
59
|
+
.crisp-share-row { display: flex; align-items: center; gap: ${c.space["2"]}; min-height: 34px; }
|
|
60
|
+
.crisp-share-row-label { flex: 1; min-width: 0; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; letter-spacing: -0.07px; }
|
|
61
|
+
.crisp-share-row .crisp-select { flex-shrink: 0; }
|
|
62
|
+
.crisp-share-row .crisp-select .crisp-select-value,
|
|
63
|
+
.crisp-share-row .crisp-select .crisp-select-chevron { color: ${mutedText(55)}; }
|
|
64
|
+
|
|
65
|
+
.crisp-share-clear {
|
|
66
|
+
align-self: flex-start; margin: 0; padding: 0; border: none; background: none;
|
|
67
|
+
font-family: inherit; font-size: ${c.text.md}; font-weight: ${c.weight.medium};
|
|
68
|
+
color: ${mutedText(63)}; cursor: pointer;
|
|
69
|
+
}
|
|
70
|
+
.crisp-share-clear:hover { color: ${c.fgPrimary}; text-decoration: underline; }
|
|
71
|
+
|
|
72
|
+
.crisp-share-section-label {
|
|
73
|
+
margin: 0; font-size: ${c.text.sm}; font-weight: ${c.weight.medium}; color: ${mutedText(50)};
|
|
74
|
+
}
|
|
75
|
+
.crisp-share-members { display: flex; flex-direction: column; gap: ${c.space["2"]}; }
|
|
76
|
+
.crisp-share-member { display: flex; align-items: center; gap: ${c.space["2"]}; }
|
|
77
|
+
.crisp-share-member-text { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: ${c.space["1.5"]}; overflow: hidden; }
|
|
78
|
+
.crisp-share-member-name { flex-shrink: 0; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${c.fgPrimary}; letter-spacing: -0.07px; }
|
|
79
|
+
.crisp-share-member-email { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${mutedText(55)}; letter-spacing: -0.07px; }
|
|
80
|
+
.crisp-share-member .crisp-select { flex-shrink: 0; }
|
|
81
|
+
.crisp-share-member .crisp-select .crisp-select-value,
|
|
82
|
+
.crisp-share-member .crisp-select .crisp-select-chevron { color: ${mutedText(55)}; }
|
|
83
|
+
|
|
84
|
+
.crisp-share-automations-row { display: flex; align-items: center; gap: ${c.space["2"]}; }
|
|
85
|
+
.crisp-share-automations-row .crisp-input { flex: 1; min-width: 0; }
|
|
86
|
+
.crisp-share-automations-row .crisp-select { flex-shrink: 0; }
|
|
87
|
+
|
|
88
|
+
.crisp-share-add {
|
|
89
|
+
flex-shrink: 0; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center;
|
|
90
|
+
height: 32px; padding: 0 ${c.space["2.5"]}; border: none; border-radius: ${c.radius.lg};
|
|
91
|
+
background: ${c.bgBrandSolid}; color: ${c.fgOnBrand};
|
|
92
|
+
font-family: inherit; font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; letter-spacing: -0.07px;
|
|
93
|
+
cursor: pointer; transition: background-color .12s ease, opacity .12s ease;
|
|
94
|
+
}
|
|
95
|
+
.crisp-share-add:hover:not(:disabled) { background: ${c.bgBrandSolidPressed}; }
|
|
96
|
+
.crisp-share-add:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
97
|
+
|
|
98
|
+
.crisp-share-learnmore {
|
|
99
|
+
display: inline-flex; align-items: center; gap: ${c.space["1.5"]};
|
|
100
|
+
font-size: ${c.text.lg}; font-weight: ${c.weight.medium}; color: ${mutedText(63)};
|
|
101
|
+
text-decoration: none;
|
|
102
|
+
}
|
|
103
|
+
.crisp-share-learnmore:hover { color: ${c.fgPrimary}; }
|
|
104
|
+
.crisp-share-learnmore svg { flex-shrink: 0; }
|
|
105
|
+
`,
|
|
106
|
+
variants: {},
|
|
107
|
+
defaultVariants: {},
|
|
108
|
+
compoundVariants: [],
|
|
109
|
+
});
|
|
110
|
+
export const shareRecipe = resolveRecipe(config);
|
|
111
|
+
export default config;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./shortcutssheet.js";
|