@crisp-ui-kit/crisp 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/core/caret-rect.js +73 -0
- package/dist/cjs/core/index.js +7 -1
- package/dist/cjs/core/mention.js +32 -0
- package/dist/cjs/core/use-mention-trigger.js +124 -0
- package/dist/cjs/product/recordpanel/recordpanel.js +13 -81
- package/dist/cjs/product/recordpanel/recordpanel.recipe.js +53 -44
- package/dist/cjs/product/recordstable/editable-field.js +11 -8
- package/dist/cjs/product/recordstable/recordstable.tag.js +8 -3
- package/dist/cjs/product/richtext/index.js +2 -1
- package/dist/cjs/product/richtext/richtext.js +440 -23
- package/dist/cjs/product/richtext/richtext.recipe.js +59 -9
- package/dist/esm/core/caret-rect.js +70 -0
- package/dist/esm/core/index.js +3 -0
- package/dist/esm/core/mention.js +29 -0
- package/dist/esm/core/use-mention-trigger.js +88 -0
- package/dist/esm/product/recordpanel/index.js +1 -1
- package/dist/esm/product/recordpanel/recordpanel.js +15 -83
- package/dist/esm/product/recordpanel/recordpanel.recipe.js +54 -45
- package/dist/esm/product/recordstable/editable-field.js +11 -8
- package/dist/esm/product/recordstable/recordstable.tag.js +8 -3
- package/dist/esm/product/richtext/index.js +1 -1
- package/dist/esm/product/richtext/richtext.js +439 -22
- package/dist/esm/product/richtext/richtext.recipe.js +60 -10
- package/dist/recordpanel.css +47 -41
- package/dist/recordpanel.layered.css +47 -41
- package/dist/richtext.css +31 -5
- package/dist/richtext.layered.css +31 -5
- package/dist/styles.css +79 -47
- package/dist/styles.layered.css +79 -47
- package/dist/types/core/caret-rect.d.ts +16 -0
- package/dist/types/core/index.d.ts +3 -0
- package/dist/types/core/mention.d.ts +27 -0
- package/dist/types/core/use-mention-trigger.d.ts +41 -0
- package/dist/types/product/recordpanel/index.d.ts +1 -1
- package/dist/types/product/recordpanel/recordpanel.d.ts +81 -27
- package/dist/types/product/recordstable/editable-field.d.ts +11 -8
- package/dist/types/product/recordstable/recordstable.tag.d.ts +8 -3
- package/dist/types/product/richtext/index.d.ts +1 -1
- package/dist/types/product/richtext/richtext.d.ts +168 -7
- package/package.json +1 -1
package/dist/styles.layered.css
CHANGED
|
@@ -2239,52 +2239,58 @@
|
|
|
2239
2239
|
position: absolute; top: var(--crisp-space-2); inset-inline-end: var(--crisp-space-2); bottom: var(--crisp-space-2); z-index: 20; display: flex; flex-direction: column; overflow: hidden; width: 420px; max-width: calc(92% - var(--crisp-space-2)); background: var(--crisp-bg-surface); border-radius: var(--crisp-radius-xl); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); font-family: var(--crisp-font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; transform: translateX(0); transition: transform .2s ease;
|
|
2240
2240
|
@starting-style { transform: translateX(calc(100% + var(--crisp-space-2))); }
|
|
2241
2241
|
& * { box-sizing: border-box; }
|
|
2242
|
-
|
|
2242
|
+
/* #448: measured live on app.attio.com's peek -- a toolbar row above
|
|
2243
|
+
the identity header: Close, then Previous/Next, on the left; "Open
|
|
2244
|
+
record" on the right. 49px tall, 8px/10px padding, a hairline border
|
|
2245
|
+
beneath (the same borderSubtle every other panel/row edge here
|
|
2246
|
+
already uses). Close keeps the existing transparent ghost button;
|
|
2247
|
+
Previous/Next/Open record are a distinct field-elevation style
|
|
2248
|
+
(white fill, e.field's own ring+shadow, solid icon) -- measured as genuinely different chrome from Close, not a reuse. */
|
|
2249
|
+
& .crisp-recordpanel-toolbar { display: flex; align-items: center; height: 49px; flex-shrink: 0; padding: var(--crisp-space-2) var(--crisp-space-2-5); border-bottom: 1px solid var(--crisp-border-subtle); }
|
|
2250
|
+
& .crisp-recordpanel-toolbar-spacer { flex: 1; }
|
|
2251
|
+
& .crisp-recordpanel-pager { display: flex; align-items: center; gap: var(--crisp-space-1-5); margin-inline-start: var(--crisp-space-3); }
|
|
2252
|
+
& .crisp-recordpanel-iconbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: transparent; color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); font-family: inherit; cursor: pointer; transition: background-color .12s; }
|
|
2253
|
+
& .crisp-recordpanel-iconbtn:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
|
|
2254
|
+
& .crisp-recordpanel-navbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-neutral), 0 0 2px rgba(28,40,64,0.18), 0 1px 3px rgba(0,0,0,0.04); color: var(--crisp-fg-primary); font-family: inherit; cursor: pointer; }
|
|
2255
|
+
& .crisp-recordpanel-navbtn:hover:not(:disabled) { background: var(--crisp-bg-raised); }
|
|
2256
|
+
& .crisp-recordpanel-navbtn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
2257
|
+
& .crisp-recordpanel-header { display: flex; align-items: center; gap: var(--crisp-space-2); height: 52px; flex-shrink: 0; padding: 0 var(--crisp-space-3); }
|
|
2243
2258
|
& .crisp-recordpanel-fav { width: 22px; height: var(--crisp-size-chip); border-radius: var(--crisp-radius-sm); flex-shrink: 0; object-fit: cover; background: var(--crisp-bg-raised); }
|
|
2244
2259
|
& .crisp-recordpanel-title { flex: 1; min-width: 0; font-size: var(--crisp-text-xl); font-weight: var(--crisp-weight-semibold); letter-spacing: -0.2px; color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2245
|
-
& .crisp-recordpanel-iconbtn { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; flex-shrink: 0; padding: 0; border: none; border-radius: var(--crisp-radius-md); background: transparent; color: var(--crisp-fg-muted); font-family: inherit; cursor: pointer; transition: background-color .12s; }
|
|
2246
|
-
& .crisp-recordpanel-iconbtn:hover { background: var(--crisp-bg-raised); color: var(--crisp-fg-primary); }
|
|
2247
2260
|
& .crisp-recordpanel-body { flex: 1; min-height: 0; overflow-y: auto; padding: var(--crisp-space-2) var(--crisp-space-3); }
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
& .crisp-recordpanel-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
& .crisp-recordpanel-
|
|
2268
|
-
& .crisp-recordpanel-
|
|
2269
|
-
& .crisp-recordpanel-
|
|
2270
|
-
& .crisp-recordpanel-
|
|
2271
|
-
& .crisp-recordpanel-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
& .crisp-recordstable-tag[data-tone="purple"] { background: color-mix(in srgb, var(--crisp-bg-purple) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-purple) 80%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-purple) 30%, transparent); }
|
|
2280
|
-
& .crisp-recordstable-tag[data-tone="cyan"] { background: color-mix(in srgb, var(--crisp-bg-cyan) 18%, transparent); color: color-mix(in srgb, var(--crisp-bg-cyan) 82%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-cyan) 30%, transparent); }
|
|
2281
|
-
& .crisp-recordstable-tag[data-tone="orange"] { background: color-mix(in srgb, var(--crisp-bg-orange) 16%, transparent); color: color-mix(in srgb, var(--crisp-bg-orange) 82%, var(--crisp-fg-primary)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-bg-orange) 30%, transparent); }
|
|
2282
|
-
& .crisp-recordpanel-link { color: var(--crisp-bg-brand-solid); text-decoration: none; overflow: hidden; text-overflow: ellipsis; }
|
|
2283
|
-
& .crisp-recordpanel-link:hover { text-decoration: underline; }
|
|
2284
|
-
|
|
2285
|
-
/* #329: an optional slot for record actions (e.g. Edit). Reuses the
|
|
2286
|
-
header's own measured values rather than inventing new ones -- the same horizontal inset (space.3), the same hairline border token (borderSubtle, flipped to border-top since this sits at the opposite edge), and the header/body gap (space.2) for its own vertical padding and for spacing between whatever it's given. */
|
|
2261
|
+
/* #448: measured live on app.attio.com's peek (hover a Companies row,
|
|
2262
|
+
click "Open preview"). Section label reads identically for both
|
|
2263
|
+
Highlights and Activity: 14px/500, the same 63%-of-fgPrimary mix
|
|
2264
|
+
RecordPage's own field-label already uses (product/recordpage's
|
|
2265
|
+
.crisp-recordpage-field-label) -- one more surface landing on the
|
|
2266
|
+
same measured "muted section heading" colour. */
|
|
2267
|
+
& .crisp-recordpanel-section { padding: var(--crisp-space-2) var(--crisp-space-1) 0; }
|
|
2268
|
+
& .crisp-recordpanel-section-head { display: flex; align-items: center; justify-content: space-between; gap: var(--crisp-space-2); height: 20px; margin-bottom: var(--crisp-space-2); }
|
|
2269
|
+
& .crisp-recordpanel-section-label { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
|
|
2270
|
+
/* "View all" -- measured 10px/400, a lighter mix than the section label
|
|
2271
|
+
(55%, not 63%) -- its own distinct, honestly-measured value, not a
|
|
2272
|
+
reuse of a nearby one that happened to be close. */
|
|
2273
|
+
& .crisp-recordpanel-section-action { border: none; background: none; padding: 0; font-family: inherit; font-size: 10px; font-weight: var(--crisp-weight-regular); color: color-mix(in srgb, var(--crisp-fg-primary) 55%, transparent); cursor: pointer; }
|
|
2274
|
+
& .crisp-recordpanel-section-action:hover { color: var(--crisp-fg-primary); }
|
|
2275
|
+
|
|
2276
|
+
/* Highlights: a 2-column grid of label+icon+value cards, no card
|
|
2277
|
+
chrome at all (no border/background/radius/shadow -- confirmed on
|
|
2278
|
+
the live DOM: every highlight's computed style is fully transparent). 8px row/column gap; two rows measured "233x86" inside 473px content width is exactly (473 - 8) / 2. */
|
|
2279
|
+
& .crisp-recordpanel-highlights { display: grid; grid-template-columns: 1fr 1fr; gap: var(--crisp-space-2); }
|
|
2280
|
+
& .crisp-recordpanel-highlight { display: flex; flex-direction: column; gap: var(--crisp-space-1-5); min-width: 0; }
|
|
2281
|
+
& .crisp-recordpanel-highlight-label { display: flex; align-items: center; gap: var(--crisp-space-1); font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
|
|
2282
|
+
& .crisp-recordpanel-highlight-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2283
|
+
& .crisp-recordpanel-highlight-label svg { flex-shrink: 0; color: var(--crisp-fg-muted); }
|
|
2284
|
+
& .crisp-recordpanel-highlight-value { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); overflow: hidden; text-overflow: ellipsis; }
|
|
2285
|
+
/* Activity is a consumer-supplied slot (crisp has no activity-feed model of its own to render) -- this only wraps it with the measured section heading above. */
|
|
2286
|
+
& .crisp-recordpanel-activity { font-size: var(--crisp-text-lg); }
|
|
2287
|
+
/* #329: an optional slot for record actions (e.g. Edit). Reuses the header's own measured values rather than inventing new ones -- the
|
|
2288
|
+
same horizontal inset (space.3), the same hairline border token
|
|
2289
|
+
(borderSubtle, flipped to border-top since this sits at the opposite
|
|
2290
|
+
edge), and the header/body gap (space.2) for its own vertical padding
|
|
2291
|
+
and for spacing between whatever it's given. */
|
|
2287
2292
|
& .crisp-recordpanel-footer { flex-shrink: 0; display: flex; align-items: center; gap: var(--crisp-space-2); padding: var(--crisp-space-2) var(--crisp-space-3); border-top: 1px solid var(--crisp-border-subtle); }
|
|
2293
|
+
[dir="rtl"] & [data-rtl-flip] { transform: scaleX(-1); }
|
|
2288
2294
|
}
|
|
2289
2295
|
|
|
2290
2296
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -2532,12 +2538,38 @@
|
|
|
2532
2538
|
& .crisp-rte-body code { font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; font-size: var(--crisp-text-md); padding: 1px var(--crisp-space-1); border-radius: var(--crisp-radius-sm); background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
|
|
2533
2539
|
& .crisp-rte-body a { color: var(--crisp-bg-brand-solid); text-decoration: underline; text-underline-offset: 2px; }
|
|
2534
2540
|
& .crisp-rte-body strong { font-weight: var(--crisp-weight-semibold); }
|
|
2535
|
-
& .crisp-rte-
|
|
2541
|
+
& .crisp-rte-body img { display: block; max-width: 100%; border-radius: var(--crisp-radius-md); }
|
|
2542
|
+
/* Not measured against Attio (#428) — nothing in a read-only pass over app.attio.com had a live @mention rendered to read computed styles from, and typing one to create one is off-limits there. Reused verbatim from the one measured "brand" tag/chip this repo already has (RecordsTable/MultiSelect's [data-tone="brand"]) rather than
|
|
2543
|
+
inventing a new color or shape. */
|
|
2544
|
+
& .crisp-rte-mention { display: inline; border-radius: var(--crisp-radius-chip); padding: 0 var(--crisp-space-1); background: color-mix(in srgb, var(--crisp-bg-brand-solid) 14%, transparent); color: color-mix(in srgb, var(--crisp-bg-brand-solid) 82%, var(--crisp-fg-primary)); font-weight: var(--crisp-weight-medium); cursor: default; }
|
|
2545
|
+
|
|
2546
|
+
& .crisp-rte-toolbar { position: fixed; z-index: 70; display: inline-flex; align-items: center; gap: 2px; padding: 2px; border-radius: 10px; background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); transform: translate(-50%, -100%); opacity: 1; transition: opacity 0.1s ease; }
|
|
2536
2547
|
& .crisp-rte-toolbar { @starting-style { opacity: 0; } }
|
|
2537
|
-
& .crisp-rte-tool { display: inline-flex; align-items: center; justify-content: center; width:
|
|
2538
|
-
& .crisp-rte-tool
|
|
2539
|
-
& .crisp-rte-tool
|
|
2540
|
-
& .crisp-rte-tool-
|
|
2548
|
+
& .crisp-rte-tool { display: inline-flex; align-items: center; justify-content: center; gap: var(--crisp-space-1); min-width: 30px; height: 28px; padding: 0 7px; margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); font-family: inherit; font-size: var(--crisp-text-sm); font-weight: var(--crisp-weight-medium); cursor: pointer; transition: background-color .1s; }
|
|
2549
|
+
& .crisp-rte-tool--labelled { width: auto; }
|
|
2550
|
+
& .crisp-rte-tool:hover { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); color: var(--crisp-fg-primary); }
|
|
2551
|
+
& .crisp-rte-tool[data-active], & .crisp-rte-tool[aria-expanded="true"] { background: color-mix(in srgb, var(--crisp-bg-brand-solid) 12%, transparent); color: var(--crisp-bg-brand-solid); }
|
|
2552
|
+
& .crisp-rte-tool-sep { width: 1px; height: 16px; margin: 0 2px; background: var(--crisp-border-subtle); }
|
|
2553
|
+
|
|
2554
|
+
& .crisp-rte-swatchpanel { position: fixed; z-index: 71; display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--crisp-space-1); width: 168px; padding: var(--crisp-space-1-5); border-radius: var(--crisp-radius-lg); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); transform: translate(-50%, var(--crisp-space-1-5)); opacity: 1; transition: opacity 0.1s ease; }
|
|
2555
|
+
& .crisp-rte-swatchpanel { @starting-style { opacity: 0; } }
|
|
2556
|
+
& .crisp-rte-swatch { width: 28px; height: 28px; padding: 0; margin: 0; border: none; border-radius: 50%; background: none; font-family: inherit; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--crisp-fg-primary) 10%, transparent); cursor: pointer; }
|
|
2557
|
+
& .crisp-rte-swatch--none { background: var(--crisp-bg-surface); position: relative; }
|
|
2558
|
+
& .crisp-rte-swatch--none::after { content: ""; position: absolute; inset: 6px; border-top: 1.5px solid color-mix(in srgb, var(--crisp-fg-primary) 40%, transparent); transform: rotate(-45deg); }
|
|
2559
|
+
|
|
2560
|
+
& .crisp-rte-blockmenu { position: fixed; z-index: 71; box-sizing: border-box; width: 356px; padding: 0; border-radius: var(--crisp-radius-xl); background: var(--crisp-bg-surface); box-shadow: inset 0 0 0 1px var(--crisp-ring-popover), 0 0 0 1px var(--crisp-shadow-popover1), 0 4px 8px -4px var(--crisp-shadow-popover2), 0 4px 12px -2px var(--crisp-shadow-popover3); opacity: 1; transition: opacity 0.1s ease; }
|
|
2561
|
+
& .crisp-rte-blockmenu { @starting-style { opacity: 0; } }
|
|
2562
|
+
& .crisp-rte-blockmenu-search { display: flex; align-items: center; gap: var(--crisp-space-1-5); padding: var(--crisp-space-2-5) var(--crisp-space-3); border-bottom: 1px solid var(--crisp-border-subtle); color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
|
|
2563
|
+
& .crisp-rte-blockmenu-input { flex: 1; border: none; outline: none; background: none; font-family: inherit; font-size: var(--crisp-text-lg); height: 20px; color: var(--crisp-fg-primary); }
|
|
2564
|
+
& .crisp-rte-blockmenu-input::placeholder { color: color-mix(in srgb, var(--crisp-fg-primary) 40%, transparent); }
|
|
2565
|
+
& .crisp-rte-blockmenu-list { display: flex; flex-direction: column; padding: var(--crisp-space-1-5); gap: var(--crisp-space-px); }
|
|
2566
|
+
& .crisp-rte-blockmenu-item { display: flex; align-items: center; gap: var(--crisp-space-1-5); height: 52px; padding: var(--crisp-space-1-5) var(--crisp-space-2); margin: 0; border: none; border-radius: var(--crisp-radius-md); background: none; font-family: inherit; text-align: start; cursor: pointer; }
|
|
2567
|
+
& .crisp-rte-blockmenu-item svg { flex-shrink: 0; color: color-mix(in srgb, var(--crisp-fg-primary) 63%, transparent); }
|
|
2568
|
+
& .crisp-rte-blockmenu-item[data-highlighted] { background: color-mix(in srgb, var(--crisp-fg-primary) 6%, transparent); }
|
|
2569
|
+
& .crisp-rte-blockmenu-item-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
2570
|
+
& .crisp-rte-blockmenu-item-label { font-size: var(--crisp-text-lg); font-weight: var(--crisp-weight-medium); color: var(--crisp-fg-primary); }
|
|
2571
|
+
& .crisp-rte-blockmenu-item-desc { font-size: var(--crisp-text-sm); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
|
|
2572
|
+
& .crisp-rte-blockmenu-empty { padding: var(--crisp-space-4) var(--crisp-space-3); text-align: center; font-size: var(--crisp-text-md); color: color-mix(in srgb, var(--crisp-fg-primary) 50%, transparent); }
|
|
2541
2573
|
}
|
|
2542
2574
|
|
|
2543
2575
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pixel position of a caret index inside a `<textarea>`/`<input>`, in
|
|
3
|
+
* viewport coordinates — where a mention picker anchors.
|
|
4
|
+
*
|
|
5
|
+
* Neither element exposes caret geometry directly, so this uses the standard
|
|
6
|
+
* mirror-div technique: an off-screen div replicating every computed style
|
|
7
|
+
* that affects text layout (font, padding, border, wrapping), holding the
|
|
8
|
+
* same text up to the caret plus a marker span. The marker's own
|
|
9
|
+
* `getBoundingClientRect()`, translated back onto the real field's box, is
|
|
10
|
+
* the caret's actual rendered position — nothing here guesses at line
|
|
11
|
+
* height or character width itself.
|
|
12
|
+
*/
|
|
13
|
+
export declare function caretRectInField(field: HTMLTextAreaElement | HTMLInputElement, index: number): {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { caretRectInField } from "./caret-rect";
|
|
1
2
|
export { type CellDatePrecision, cellDatePrecision, formatCellDate, parseCellDate, } from "./cell-date";
|
|
2
3
|
export { cx } from "./cx";
|
|
3
4
|
export { defineRecipe } from "./define";
|
|
@@ -6,6 +7,7 @@ export { isRtlElement } from "./direction";
|
|
|
6
7
|
export { FloatingPortal } from "./floating-portal";
|
|
7
8
|
export { hasOpenFloatingLayer, isInsideLaterFloatingLayer, registerFloatingLayer, } from "./floating-stack";
|
|
8
9
|
export { formValueStyle } from "./form-value";
|
|
10
|
+
export { type MentionAnchor, type MentionMatch, matchMentionTrigger, } from "./mention";
|
|
9
11
|
export { type FloatingAlign, type PlaceFloatingOptions, placeFloating, } from "./place-floating";
|
|
10
12
|
export { Primitive } from "./primitive";
|
|
11
13
|
export { resolveRecipe } from "./resolve";
|
|
@@ -15,4 +17,5 @@ export type * from "./types";
|
|
|
15
17
|
export { useFocusAfterRemoval } from "./use-focus-after-removal";
|
|
16
18
|
export { useFormReset } from "./use-form-reset";
|
|
17
19
|
export { useLabelAssociation } from "./use-label-association";
|
|
20
|
+
export { type MentionTrigger, type MentionTriggerState, type UseMentionTriggerOptions, useMentionTrigger, } from "./use-mention-trigger";
|
|
18
21
|
export { usePrefersReducedMotion } from "./use-prefers-reduced-motion";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type MentionMatch = {
|
|
2
|
+
/** Text typed after the trigger character — what a picker filters by. */
|
|
3
|
+
query: string;
|
|
4
|
+
/** Index, in the text passed in, where the trigger character itself sits. */
|
|
5
|
+
start: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Is there an active "@query" mention trigger ending at the caret?
|
|
9
|
+
*
|
|
10
|
+
* Matches the rule every "@mention" implementation (GitHub, Slack, Linear)
|
|
11
|
+
* actually uses: the trigger character must start a word — the very first
|
|
12
|
+
* character, or preceded by whitespace — and nothing between it and the
|
|
13
|
+
* caret may contain whitespace. `foo@bar` is an email/handle stuck to a word,
|
|
14
|
+
* not a trigger; `@jo next` has already moved past the mention it typed.
|
|
15
|
+
*
|
|
16
|
+
* Takes plain text rather than a live DOM node so it is one pure function
|
|
17
|
+
* usable against a `<textarea>`'s `value` and a `contentEditable` text
|
|
18
|
+
* node's content alike, and is the one thing here worth unit-testing on its
|
|
19
|
+
* own — everything downstream of it (caret geometry, DOM insertion) needs a
|
|
20
|
+
* real browser to verify.
|
|
21
|
+
*/
|
|
22
|
+
export declare function matchMentionTrigger(textBeforeCaret: string, trigger?: string): MentionMatch | null;
|
|
23
|
+
/** A caret's position in viewport coordinates — where a picker anchors. */
|
|
24
|
+
export type MentionAnchor = {
|
|
25
|
+
x: number;
|
|
26
|
+
y: number;
|
|
27
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type MentionAnchor } from "./mention";
|
|
3
|
+
export type MentionTriggerState = {
|
|
4
|
+
open: boolean;
|
|
5
|
+
query: string;
|
|
6
|
+
/** Where to anchor a picker — null while closed. */
|
|
7
|
+
anchor: MentionAnchor | null;
|
|
8
|
+
};
|
|
9
|
+
export type UseMentionTriggerOptions = {
|
|
10
|
+
/** Character that opens the picker. Default `"@"`. */
|
|
11
|
+
trigger?: string;
|
|
12
|
+
};
|
|
13
|
+
export type MentionTrigger = MentionTriggerState & {
|
|
14
|
+
/** Dismiss the picker without inserting anything — e.g. on Escape. */
|
|
15
|
+
close: () => void;
|
|
16
|
+
/** Replace the "@query" text (the trigger included) with `text`, restore
|
|
17
|
+
* focus and the caret right after it, and dismiss. */
|
|
18
|
+
insert: (text: string) => void;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Watches a `<textarea>`/`<input>` for an "@query" mention trigger and
|
|
22
|
+
* reports where to anchor a consumer-supplied picker (#372).
|
|
23
|
+
*
|
|
24
|
+
* crisp draws no picker of its own here — GitHub/Slack/Linear-style @mention
|
|
25
|
+
* UI varies too much by app (avatars, groups, recent-first ordering, a
|
|
26
|
+
* "create new person" row) to have one measured shape, and this repo's rule
|
|
27
|
+
* is measure-first, never invented polish. What is shared and worth owning
|
|
28
|
+
* centrally is the fiddly, easy-to-get-wrong part: deciding when "@" is
|
|
29
|
+
* actually a trigger rather than part of a word or an email address (see
|
|
30
|
+
* `matchMentionTrigger`), and where its picker anchors on screen (see
|
|
31
|
+
* `caretRectInField`, since neither element exposes caret geometry itself).
|
|
32
|
+
*
|
|
33
|
+
* Does not close on blur: a picker built from this needs to survive the
|
|
34
|
+
* field losing focus to a `mousedown` on one of its own rows, the same
|
|
35
|
+
* problem every floating surface in this repo solves for itself (see
|
|
36
|
+
* `RichTextEditor`'s toolbar `keepSel`, or `Popover`'s own outside-pointer-
|
|
37
|
+
* down listener) — so closing on outside interaction is left to the
|
|
38
|
+
* consumer's picker, which already knows what counts as "outside" for its
|
|
39
|
+
* own markup.
|
|
40
|
+
*/
|
|
41
|
+
export declare function useMentionTrigger(ref: React.RefObject<HTMLTextAreaElement | HTMLInputElement | null>, { trigger }?: UseMentionTriggerOptions): MentionTrigger;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { RecordPanel, type RecordPanelProps } from "./recordpanel";
|
|
1
|
+
export { RecordPanel, type RecordPanelHighlight, type RecordPanelProps, } from "./recordpanel";
|
|
2
2
|
export { recordpanelRecipe } from "./recordpanel.recipe";
|
|
@@ -1,45 +1,99 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { type
|
|
2
|
+
import { type AvatarProps } from "../../components/avatar";
|
|
3
|
+
import type { Row } from "../recordstable";
|
|
4
|
+
type IconType = React.ComponentType<{
|
|
5
|
+
size?: number;
|
|
6
|
+
strokeWidth?: number;
|
|
7
|
+
}>;
|
|
8
|
+
/** One Highlights card — label, value and an optional icon, laid out the
|
|
9
|
+
* way Attio's peek panel lays its own out (measured live, #448): a 2-column
|
|
10
|
+
* grid, no card chrome (no border/background/shadow), a muted label row
|
|
11
|
+
* with a trailing icon, a filled-colour value below it. */
|
|
12
|
+
export type RecordPanelHighlight = {
|
|
13
|
+
key: string;
|
|
14
|
+
label: string;
|
|
15
|
+
value: React.ReactNode;
|
|
16
|
+
icon?: IconType;
|
|
17
|
+
};
|
|
3
18
|
export type RecordPanelProps = Omit<React.ComponentPropsWithoutRef<"div">, "children" | "title"> & {
|
|
4
|
-
/** The record to show; render nothing when null. */
|
|
5
19
|
/** Label for the close control. */
|
|
6
20
|
closeLabel?: string;
|
|
21
|
+
/**
|
|
22
|
+
* A toolbar row above the panel's own identity header — measured live on
|
|
23
|
+
* app.attio.com's peek (hover a Companies row, click "Open preview"):
|
|
24
|
+
* Close, then Previous/Next record on the left, an "Open record" icon on
|
|
25
|
+
* the right, a hairline border beneath separating it from the identity
|
|
26
|
+
* block below. Close always renders (`onClose` is required); Previous/
|
|
27
|
+
* Next/Open record each render only when given a handler — a consumer
|
|
28
|
+
* with no adjacent-row concept, or nowhere to open the record's own full
|
|
29
|
+
* page, simply omits them, leaving just Close.
|
|
30
|
+
*/
|
|
31
|
+
onOpenRecord?: () => void;
|
|
32
|
+
openRecordLabel?: string;
|
|
33
|
+
onPrevious?: () => void;
|
|
34
|
+
previousRecordLabel?: string;
|
|
35
|
+
onNext?: () => void;
|
|
36
|
+
nextRecordLabel?: string;
|
|
7
37
|
/**
|
|
8
38
|
* The panel's heading, and so its accessible name (via `aria-labelledby`,
|
|
9
39
|
* not a stringified `aria-label` — it also drives the visible title, which
|
|
10
|
-
* can be any node). Defaults to
|
|
11
|
-
* this prop existed; pass it to title the panel by something else, e.g.
|
|
12
|
-
* "{Collection} record".
|
|
40
|
+
* can be any node). Defaults to `record.name`.
|
|
13
41
|
*/
|
|
14
42
|
title?: React.ReactNode;
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
record
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
/** Avatar image URL beside the title. Omit for an initials tile built
|
|
44
|
+
* from `record.name`'s first character. */
|
|
45
|
+
avatar?: string;
|
|
46
|
+
/** Tone for the initials tile when no `avatar` image is given (default
|
|
47
|
+
* "brand", matching Attio's own peek). */
|
|
48
|
+
avatarTone?: NonNullable<AvatarProps["tone"]>;
|
|
49
|
+
/** The record to show; render nothing when null. Only `id`/`name` are
|
|
50
|
+
* read directly here — everything else the panel shows comes from
|
|
51
|
+
* `highlights`/`activity`, the consumer's own data. */
|
|
52
|
+
record: (Row & {
|
|
53
|
+
name?: string;
|
|
54
|
+
}) | null;
|
|
55
|
+
/** Heading above the highlight cards (default "Highlights"). Omit the
|
|
56
|
+
* whole section by omitting `highlights`. */
|
|
57
|
+
highlightsLabel?: React.ReactNode;
|
|
24
58
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* and renders the rejection's message beside the field. A field with
|
|
31
|
-
* `editable` but no `onEdit` never opens — there's nothing to commit to.
|
|
59
|
+
* Highlight cards — label, value, optional icon (#448). Attio's own peek
|
|
60
|
+
* shows these as a fixed set of business-pipeline stats (Connection
|
|
61
|
+
* strength, Estimated ARR, …); crisp has no such fixed set, so this is
|
|
62
|
+
* fully consumer-supplied, laid out in Attio's own measured 2-column
|
|
63
|
+
* grid. Omit for no Highlights section at all.
|
|
32
64
|
*/
|
|
33
|
-
|
|
65
|
+
highlights?: RecordPanelHighlight[];
|
|
66
|
+
/** Heading above the activity feed (default "Activity"). */
|
|
67
|
+
activityLabel?: React.ReactNode;
|
|
68
|
+
/** Label for the trailing "View all" action beside the Activity heading.
|
|
69
|
+
* Only rendered when `onViewAllActivity` is given. */
|
|
70
|
+
viewAllActivityLabel?: React.ReactNode;
|
|
71
|
+
onViewAllActivity?: () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Activity feed content (#448) — a consumer-supplied slot. crisp has no
|
|
74
|
+
* activity-feed model of its own (no fixed shape for "who did what,
|
|
75
|
+
* when" — that's app data); this only wraps whatever's given with the
|
|
76
|
+
* measured "Activity" heading Attio's peek shows above its own feed. Omit
|
|
77
|
+
* for no Activity section at all.
|
|
78
|
+
*/
|
|
79
|
+
activity?: React.ReactNode;
|
|
80
|
+
/** Rendered below the panel's content, e.g. an Edit button. Omit for no footer. */
|
|
81
|
+
footer?: React.ReactNode;
|
|
82
|
+
onClose: () => void;
|
|
34
83
|
className?: string;
|
|
35
84
|
};
|
|
36
85
|
/**
|
|
37
|
-
* Right-side record peek panel —
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
86
|
+
* Right-side record peek panel — Attio's own peek shows a Highlights card
|
|
87
|
+
* area and an Activity feed (measured live: hover a Companies row, click its
|
|
88
|
+
* "Open preview" trigger), never an editable field list (#448 — a "Details"
|
|
89
|
+
* list was this component's own invention before, and #431's editable API
|
|
90
|
+
* had nothing real in this panel to attach to; that API stays on
|
|
91
|
+
* `RecordPage`'s own Details column, where Attio does let you edit in
|
|
92
|
+
* place). Render it inside a `position: relative` container (e.g. the app
|
|
93
|
+
* main area); it slides in from the right and overlays the content.
|
|
41
94
|
*/
|
|
42
|
-
export declare function RecordPanel({ record,
|
|
95
|
+
export declare function RecordPanel({ record, onClose, className, closeLabel, onOpenRecord, openRecordLabel, onPrevious, previousRecordLabel, onNext, nextRecordLabel, title, avatar, avatarTone, highlightsLabel, highlights, activityLabel, viewAllActivityLabel, onViewAllActivity, activity, footer, ...rest }: RecordPanelProps): React.JSX.Element | null;
|
|
43
96
|
export declare namespace RecordPanel {
|
|
44
97
|
var displayName: string;
|
|
45
98
|
}
|
|
99
|
+
export {};
|
|
@@ -8,18 +8,21 @@ export type EditableOption = {
|
|
|
8
8
|
color?: string;
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
|
-
* The async-commit state machine behind `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* The async-commit state machine behind `RecordPage`'s editable Details
|
|
12
|
+
* column (#431) — the same shape `DataTable`'s own `commitEdit` uses
|
|
13
|
+
* (#385/#430): a sync `onEdit` (returns `undefined`) commits and closes
|
|
14
14
|
* immediately; a Promise-returning `onEdit` keeps the field open and pending
|
|
15
15
|
* while it settles, then either closes it (resolve) or closes it AND
|
|
16
16
|
* reverts while showing the rejection's message (reject). The revert needs
|
|
17
|
-
* no extra code here either:
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* no extra code here either: `RecordPage` never mutates `record` itself, so
|
|
18
|
+
* once `editingKey` clears, the field simply renders `record[key]` again —
|
|
19
|
+
* whatever it still says.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* `RecordPanel` (the peek panel) used to share this hook too, before #448
|
|
22
|
+
* found that Attio's own peek has no editable field at all to attach it to
|
|
23
|
+
* — only `RecordPage`'s Details column does. Kept as its own module rather
|
|
24
|
+
* than folded back into `recordpage.tsx`: a second consumer turning up
|
|
25
|
+
* again is a smaller change than re-extracting it would be.
|
|
23
26
|
*/
|
|
24
27
|
export declare function useEditableField(onEdit?: (id: string, key: string, value: string) => Promise<void> | void): {
|
|
25
28
|
editingKey: string | null;
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
* `border-radius: ${c.radius.chip}`, 14px/20px at weight 500, and a 1px inset ring in the
|
|
4
4
|
* tone's colour.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* to be
|
|
8
|
-
*
|
|
6
|
+
* Its own module (not pasted into `recordstable.recipe.ts`) because it used
|
|
7
|
+
* to be shared with `RecordPanel`'s own Details field list too, and the two
|
|
8
|
+
* copies had already started to drift before that sharing existed — adding
|
|
9
|
+
* the measured inset ring meant patching the same rules twice. `RecordPanel`
|
|
10
|
+
* no longer renders a Details list (#448, Attio's own peek doesn't have
|
|
11
|
+
* one), so `RecordsTable` is this module's only consumer now; kept as its
|
|
12
|
+
* own file rather than folded back in, since a second consumer showing up
|
|
13
|
+
* again is exactly how the drift happened the first time.
|
|
9
14
|
*/
|
|
10
15
|
export declare const recordTagCss: string;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { RichTextEditor, type RichTextEditorProps, type RichTextValue, } from "./richtext";
|
|
1
|
+
export { RICHTEXT_COLOR_TONES, type RichTextColorTone, RichTextEditor, type RichTextEditorHandle, type RichTextEditorProps, type RichTextMention, type RichTextMentionTriggerState, type RichTextValue, } from "./richtext";
|
|
2
2
|
export { richtextRecipe } from "./richtext.recipe";
|