@bitbaum/chatkit 0.2.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +137 -8
  2. package/bin/chatkit-check-file-inputs.mjs +87 -0
  3. package/dist/attachments.d.ts +37 -0
  4. package/dist/attachments.d.ts.map +1 -1
  5. package/dist/attachments.js +75 -1
  6. package/dist/attachments.js.map +1 -1
  7. package/dist/composer.d.ts +4 -1
  8. package/dist/composer.d.ts.map +1 -1
  9. package/dist/composer.js +4 -2
  10. package/dist/composer.js.map +1 -1
  11. package/dist/dictation.d.ts +62 -2
  12. package/dist/dictation.d.ts.map +1 -1
  13. package/dist/dictation.js +103 -0
  14. package/dist/dictation.js.map +1 -1
  15. package/dist/image.d.ts +7 -0
  16. package/dist/image.d.ts.map +1 -0
  17. package/dist/image.js +41 -0
  18. package/dist/image.js.map +1 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/react/AttachMenu.d.ts +37 -0
  24. package/dist/react/AttachMenu.d.ts.map +1 -0
  25. package/dist/react/AttachMenu.js +70 -0
  26. package/dist/react/AttachMenu.js.map +1 -0
  27. package/dist/react/Composer.d.ts +24 -2
  28. package/dist/react/Composer.d.ts.map +1 -1
  29. package/dist/react/Composer.js +59 -10
  30. package/dist/react/Composer.js.map +1 -1
  31. package/dist/react/Screen.d.ts +37 -0
  32. package/dist/react/Screen.d.ts.map +1 -0
  33. package/dist/react/Screen.js +56 -0
  34. package/dist/react/Screen.js.map +1 -0
  35. package/dist/react/Thread.d.ts +27 -2
  36. package/dist/react/Thread.d.ts.map +1 -1
  37. package/dist/react/Thread.js +17 -4
  38. package/dist/react/Thread.js.map +1 -1
  39. package/dist/react/attach.d.ts +17 -0
  40. package/dist/react/attach.d.ts.map +1 -0
  41. package/dist/react/attach.js +16 -0
  42. package/dist/react/attach.js.map +1 -0
  43. package/dist/react/hooks.d.ts +9 -2
  44. package/dist/react/hooks.d.ts.map +1 -1
  45. package/dist/react/hooks.js +56 -5
  46. package/dist/react/hooks.js.map +1 -1
  47. package/dist/react/icons.d.ts +3 -0
  48. package/dist/react/icons.d.ts.map +1 -1
  49. package/dist/react/icons.js +3 -0
  50. package/dist/react/icons.js.map +1 -1
  51. package/dist/react/index.d.ts +4 -1
  52. package/dist/react/index.d.ts.map +1 -1
  53. package/dist/react/index.js +3 -1
  54. package/dist/react/index.js.map +1 -1
  55. package/dist/react/use-attachments.d.ts.map +1 -1
  56. package/dist/react/use-attachments.js +6 -34
  57. package/dist/react/use-attachments.js.map +1 -1
  58. package/dist/react/use-dictation.d.ts +23 -0
  59. package/dist/react/use-dictation.d.ts.map +1 -1
  60. package/dist/react/use-dictation.js +175 -13
  61. package/dist/react/use-dictation.js.map +1 -1
  62. package/dist/replies.d.ts +41 -0
  63. package/dist/replies.d.ts.map +1 -0
  64. package/dist/replies.js +84 -0
  65. package/dist/replies.js.map +1 -0
  66. package/package.json +9 -1
  67. package/styles.css +339 -4
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Suggested replies: the two to four things the person is most likely to say
3
+ * next, as buttons under the answer. One tap sends it.
4
+ *
5
+ * From OrangeCat's Cat, where it was the most-used control in the chat and
6
+ * existed nowhere else in the fleet. The model writes the replies itself, at
7
+ * the end of its answer, in a fenced block:
8
+ *
9
+ * ```quick_replies
10
+ * ["Yes, set it up", "Show me the numbers first", "Not now"]
11
+ * ```
12
+ *
13
+ * Asking the model in the same turn costs no second call and no latency, and
14
+ * the replies are written by the thing that knows what it just asked. This
15
+ * file is the one place the format lives: the instruction that asks for it,
16
+ * and the parser that takes it back out — including while it is still
17
+ * streaming, so a reader never watches raw JSON appear under an answer.
18
+ */
19
+ export const REPLIES_FENCE = "quick_replies";
20
+ /**
21
+ * Append to a system prompt. Written so a model leaves the block off when no
22
+ * reply is likely — a button row on every answer is noise, and noise is what
23
+ * makes people stop reading buttons.
24
+ */
25
+ export const REPLIES_INSTRUCTION = [
26
+ "## Suggested replies",
27
+ "When the person's next message is predictable — you asked a question, offered a choice, or there is an obvious next step — end your answer with a fenced block of 2–4 short replies they could tap instead of typing:",
28
+ "```" + REPLIES_FENCE,
29
+ '["Yes, do it", "Show me first", "Not now"]',
30
+ "```",
31
+ "Rules: write them in the person's language, in their voice (what THEY would say, not what you would). Each under 40 characters. Each must make sense sent on its own. Never repeat what they already said. Put the block last, after everything else. Leave it out entirely when no reply is likely, or when the conversation is over.",
32
+ ].join("\n");
33
+ const COMPLETE = new RegExp("\\n?```" + REPLIES_FENCE + "\\s*([\\s\\S]*?)```", "i");
34
+ // Still streaming: the fence has opened (or is opening) and not yet closed.
35
+ const OPEN_TAIL = new RegExp("\\n?```(?:" + REPLIES_FENCE + "[\\s\\S]*|q[a-z_]*)?$", "i");
36
+ function clean(raw, { max = 4, maxLength = 48 }) {
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(raw.trim());
40
+ }
41
+ catch {
42
+ // A model sometimes writes one reply per line instead of a JSON array.
43
+ parsed = raw
44
+ .split(/\r?\n/)
45
+ .map((l) => l.replace(/^\s*(?:[-*•]|\d+[.)])\s*/, "").replace(/^["']|["'],?$/g, ""));
46
+ }
47
+ if (!Array.isArray(parsed))
48
+ return [];
49
+ const seen = new Set();
50
+ const out = [];
51
+ for (const item of parsed) {
52
+ if (typeof item !== "string")
53
+ continue;
54
+ const reply = item.trim();
55
+ const key = reply.toLowerCase();
56
+ if (!reply || reply.length > maxLength || seen.has(key))
57
+ continue;
58
+ seen.add(key);
59
+ out.push(reply);
60
+ if (out.length >= max)
61
+ break;
62
+ }
63
+ return out;
64
+ }
65
+ /**
66
+ * Split an answer into the text to show and its suggested replies. Safe on a
67
+ * partial, streaming answer: an unfinished block is hidden, never shown.
68
+ * Text with no block comes back unchanged with `replies: []`.
69
+ */
70
+ export function extractReplies(text, options = {}) {
71
+ const match = COMPLETE.exec(text);
72
+ if (match) {
73
+ const rest = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trimEnd();
74
+ return { text: rest, replies: clean(match[1] ?? "", options) };
75
+ }
76
+ const tail = OPEN_TAIL.exec(text);
77
+ // A bare ``` at the end may be the start of an ordinary code block; only
78
+ // hide it while it could still become ours.
79
+ if (tail && tail[0].trim() !== "```") {
80
+ return { text: text.slice(0, tail.index).trimEnd(), replies: [] };
81
+ }
82
+ return { text, replies: [] };
83
+ }
84
+ //# sourceMappingURL=replies.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replies.js","sourceRoot":"","sources":["../src/replies.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitbaum/chatkit",
3
- "version": "0.2.1",
3
+ "version": "0.8.0",
4
4
  "description": "One chat for every product: a composer with a microphone that always works, a thread that does not fight the reader, messages with copy and retry, and LLM markdown \u2014 the fleet's chat standard as a package.",
5
5
  "license": "MIT",
6
6
  "author": "Cato",
@@ -36,8 +36,12 @@
36
36
  },
37
37
  "files": [
38
38
  "dist",
39
+ "bin",
39
40
  "styles.css"
40
41
  ],
42
+ "bin": {
43
+ "chatkit-check-file-inputs": "./bin/chatkit-check-file-inputs.mjs"
44
+ },
41
45
  "exports": {
42
46
  ".": {
43
47
  "types": "./dist/index.d.ts",
@@ -47,6 +51,10 @@
47
51
  "types": "./dist/react/index.d.ts",
48
52
  "default": "./dist/react/index.js"
49
53
  },
54
+ "./attach": {
55
+ "types": "./dist/react/attach.d.ts",
56
+ "default": "./dist/react/attach.js"
57
+ },
50
58
  "./styles.css": "./styles.css"
51
59
  },
52
60
  "scripts": {
package/styles.css CHANGED
@@ -20,6 +20,7 @@
20
20
  /* Defaults at ZERO specificity on the root: an app's own :root (or any
21
21
  container) rule setting a --ck-* variable always wins. Declaring these on
22
22
  the chat's elements instead would silently override the app. */
23
+ --ck-page: hsl(var(--surface-page, 0 0% 99%));
23
24
  --ck-bg: hsl(var(--surface-base, 0 0% 100%));
24
25
  --ck-raised: hsl(var(--surface-raised, 0 0% 96%));
25
26
  --ck-overlay: hsl(var(--surface-overlay, 0 0% 98%));
@@ -43,18 +44,28 @@
43
44
  --ck-text: 1rem;
44
45
  --ck-leading: 1.6;
45
46
  --ck-target: 2.75rem;
47
+ /* ChatScreen: the reading column, and the margin it keeps from the edge. */
48
+ --ck-measure: 48rem;
49
+ --ck-gutter: 1rem;
50
+ /* The person's own words. Neutral, not the accent: the accent is for the
51
+ one thing to press, and a column of tinted pills spent it on every turn. */
52
+ --ck-bubble: var(--ck-raised);
46
53
  }
47
54
 
55
+ .ck-screen,
48
56
  .ck-composer-wrap,
49
57
  .ck-thread-wrap,
50
- .ck-starters {
58
+ .ck-starters,
59
+ .ck-replies {
51
60
  font-family: var(--ck-font);
52
61
  color: var(--ck-fg);
53
62
  }
54
63
 
64
+ .ck-screen *,
55
65
  .ck-composer-wrap *,
56
66
  .ck-thread-wrap *,
57
- .ck-starters * {
67
+ .ck-starters *,
68
+ .ck-replies * {
58
69
  box-sizing: border-box;
59
70
  }
60
71
 
@@ -195,7 +206,7 @@
195
206
  color: var(--ck-fg);
196
207
  border: 1px solid var(--ck-line-2);
197
208
  }
198
- :where(.ck-composer-wrap, .ck-thread-wrap, .ck-starters) button:focus-visible,
209
+ :where(.ck-composer-wrap, .ck-thread-wrap, .ck-starters, .ck-replies) button:focus-visible,
199
210
  .ck-input:focus-visible {
200
211
  outline: 2px solid var(--ck-accent);
201
212
  outline-offset: 2px;
@@ -275,6 +286,22 @@
275
286
  .ck-voice-wave > span:nth-child(5n) {
276
287
  animation-delay: -0.8s;
277
288
  }
289
+ /* the words as they are heard: one line, its end always in view, the start
290
+ fading out — the person reads the newest words, not the oldest */
291
+ .ck-voice-live {
292
+ flex: 1;
293
+ min-width: 0;
294
+ overflow: hidden;
295
+ white-space: nowrap;
296
+ font-size: 0.9375rem;
297
+ line-height: 1.4;
298
+ color: var(--ck-fg);
299
+ -webkit-mask-image: linear-gradient(to right, transparent, #000 1.75rem);
300
+ mask-image: linear-gradient(to right, transparent, #000 1.75rem);
301
+ }
302
+ .ck-voice-live-dim {
303
+ color: var(--ck-fg-3);
304
+ }
278
305
  .ck-voice-timer {
279
306
  flex-shrink: 0;
280
307
  font-size: 0.9375rem;
@@ -309,6 +336,25 @@
309
336
  flex: 1;
310
337
  min-width: 0;
311
338
  }
339
+ .ck-problem-detail {
340
+ color: var(--ck-muted);
341
+ }
342
+ .ck-problem-retry {
343
+ flex-shrink: 0;
344
+ min-height: 2rem;
345
+ padding: 0 0.75rem;
346
+ border: 1px solid var(--ck-line-2);
347
+ border-radius: 999px;
348
+ background: transparent;
349
+ color: var(--ck-fg);
350
+ font: inherit;
351
+ font-size: 0.875rem;
352
+ cursor: pointer;
353
+ }
354
+ .ck-problem-retry:disabled {
355
+ opacity: 0.5;
356
+ cursor: default;
357
+ }
312
358
  .ck-problem-x {
313
359
  display: inline-flex;
314
360
  flex-shrink: 0;
@@ -431,7 +477,7 @@
431
477
  max-width: min(85%, 42rem);
432
478
  padding: 0.625rem 1rem;
433
479
  border-radius: calc(var(--ck-radius) * 1.25);
434
- background: var(--ck-accent-soft);
480
+ background: var(--ck-bubble);
435
481
  color: var(--ck-fg);
436
482
  font-size: var(--ck-text);
437
483
  line-height: var(--ck-leading);
@@ -553,6 +599,156 @@
553
599
  box-shadow: 0 6px 20px -8px rgb(0 0 0 / 0.35);
554
600
  }
555
601
 
602
+ /* ── screen ─────────────────────────────────────────────────────────────
603
+ The whole display is the conversation; header and composer float over it.
604
+ --ck-top-inset / --ck-bottom-inset are measured by ChatScreen. */
605
+ .ck-screen {
606
+ position: relative;
607
+ display: flex;
608
+ flex-direction: column;
609
+ height: 100%;
610
+ min-height: 0;
611
+ min-width: 0;
612
+ overflow: hidden;
613
+ background: var(--ck-page);
614
+ }
615
+ .ck-screen-viewport {
616
+ height: var(--ck-viewport-height, 100dvh);
617
+ }
618
+ .ck-screen-body {
619
+ position: relative;
620
+ display: flex;
621
+ flex: 1;
622
+ flex-direction: column;
623
+ min-height: 0;
624
+ min-width: 0;
625
+ }
626
+ /* Words pass under the header and fade as they do: the page colour at the
627
+ very top, nothing by the bottom of the row. No bar, no border. */
628
+ .ck-screen-header {
629
+ position: absolute;
630
+ inset: 0 0 auto;
631
+ z-index: 3;
632
+ padding: env(safe-area-inset-top, 0px) var(--ck-gutter) 0;
633
+ background: linear-gradient(
634
+ to bottom,
635
+ var(--ck-page) 0%,
636
+ color-mix(in srgb, var(--ck-page) 75%, transparent) 55%,
637
+ transparent 100%
638
+ );
639
+ pointer-events: none;
640
+ }
641
+ .ck-screen-header-row {
642
+ display: flex;
643
+ align-items: center;
644
+ gap: 0.5rem;
645
+ min-width: 0;
646
+ min-height: 3.5rem;
647
+ padding-block: 0.375rem;
648
+ }
649
+ .ck-screen-header-row > * {
650
+ pointer-events: auto;
651
+ }
652
+ .ck-screen-dock {
653
+ position: absolute;
654
+ inset: auto 0 0;
655
+ z-index: 3;
656
+ padding: 1.5rem var(--ck-gutter) calc(0.5rem + env(safe-area-inset-bottom, 0px));
657
+ background: linear-gradient(to top, var(--ck-page) 0%, var(--ck-page) 40%, transparent 100%);
658
+ pointer-events: none;
659
+ }
660
+ .ck-screen-column {
661
+ display: flex;
662
+ flex-direction: column;
663
+ gap: 0.5rem;
664
+ width: 100%;
665
+ max-width: var(--ck-measure);
666
+ margin-inline: auto;
667
+ pointer-events: auto;
668
+ }
669
+ .ck-screen-footnote {
670
+ margin: 0;
671
+ color: var(--ck-muted);
672
+ font-size: 0.75rem;
673
+ line-height: 1.4;
674
+ text-align: center;
675
+ }
676
+ /* The composer as a floating pill: see-through enough to show the words
677
+ passing under it, solid enough to type on. */
678
+ .ck-screen-dock .ck-composer {
679
+ border-color: color-mix(in srgb, var(--ck-line-2) 70%, transparent);
680
+ border-radius: 1.75rem;
681
+ background: color-mix(in srgb, var(--ck-bg) 86%, transparent);
682
+ -webkit-backdrop-filter: blur(20px) saturate(1.4);
683
+ backdrop-filter: blur(20px) saturate(1.4);
684
+ box-shadow: 0 6px 28px -12px rgb(0 0 0 / 0.28);
685
+ }
686
+ .ck-screen-dock .ck-voice-bar {
687
+ border-radius: 1.75rem;
688
+ }
689
+ /* The thread scrolls the full width (the scrollbar sits at the screen's
690
+ edge, and a swipe anywhere moves it); its column keeps a reading measure. */
691
+ .ck-screen .ck-thread {
692
+ padding: var(--ck-top-inset, 0px) var(--ck-gutter) calc(var(--ck-bottom-inset, 0px) + 0.75rem);
693
+ scroll-padding-top: var(--ck-top-inset, 0px);
694
+ scroll-padding-bottom: var(--ck-bottom-inset, 0px);
695
+ }
696
+ .ck-screen .ck-thread-inner {
697
+ width: 100%;
698
+ max-width: var(--ck-measure);
699
+ margin-inline: auto;
700
+ }
701
+ .ck-screen .ck-jump {
702
+ bottom: calc(var(--ck-bottom-inset, 0px) + 0.5rem);
703
+ z-index: 4;
704
+ }
705
+ .ck-screen-body > .ck-starters,
706
+ .ck-screen-body > .ck-thread-loading {
707
+ flex: 1;
708
+ justify-content: center;
709
+ width: 100%;
710
+ max-width: calc(var(--ck-measure) + 2 * var(--ck-gutter));
711
+ margin-inline: auto;
712
+ padding: var(--ck-top-inset, 0px) var(--ck-gutter) var(--ck-bottom-inset, 0px);
713
+ overflow-y: auto;
714
+ }
715
+
716
+ /* A control floating over the conversation: a frosted pill, the way the
717
+ header buttons sit over the words in the ChatGPT app. For an app's own
718
+ header buttons inside ChatScreen. */
719
+ .ck-glass {
720
+ display: inline-flex;
721
+ align-items: center;
722
+ justify-content: center;
723
+ gap: 0.375rem;
724
+ min-width: var(--ck-target);
725
+ min-height: var(--ck-target);
726
+ padding: 0 0.75rem;
727
+ border: 1px solid color-mix(in srgb, var(--ck-line-2) 60%, transparent);
728
+ border-radius: 999px;
729
+ background: color-mix(in srgb, var(--ck-bg) 70%, transparent);
730
+ -webkit-backdrop-filter: blur(16px) saturate(1.4);
731
+ backdrop-filter: blur(16px) saturate(1.4);
732
+ box-shadow: 0 2px 10px -4px rgb(0 0 0 / 0.18);
733
+ color: var(--ck-fg);
734
+ font: inherit;
735
+ font-size: 0.9375rem;
736
+ cursor: pointer;
737
+ }
738
+ .ck-glass:hover {
739
+ background: color-mix(in srgb, var(--ck-bg) 88%, transparent);
740
+ }
741
+ .ck-glass:focus-visible {
742
+ outline: 2px solid var(--ck-accent);
743
+ outline-offset: 2px;
744
+ }
745
+ @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
746
+ .ck-glass,
747
+ .ck-screen-dock .ck-composer {
748
+ background: var(--ck-bg);
749
+ }
750
+ }
751
+
556
752
  /* markdown: 16px, readable measure, no leaked ** or > */
557
753
  .ck-md {
558
754
  min-width: 0;
@@ -651,6 +847,38 @@
651
847
  color: var(--ck-fg);
652
848
  }
653
849
 
850
+ /* suggested replies — under the latest answer, one tap sends */
851
+ .ck-replies {
852
+ display: flex;
853
+ flex-wrap: wrap;
854
+ gap: 0.5rem;
855
+ min-width: 0;
856
+ margin-top: 0.75rem;
857
+ }
858
+ .ck-reply {
859
+ min-width: var(--ck-target);
860
+ min-height: var(--ck-target);
861
+ max-width: 100%;
862
+ padding: 0.5rem 0.875rem;
863
+ border: 1px solid var(--ck-line);
864
+ border-radius: 999px;
865
+ background: var(--ck-bg);
866
+ color: var(--ck-fg);
867
+ font: inherit;
868
+ font-size: 0.9375rem;
869
+ line-height: 1.25;
870
+ text-align: left;
871
+ overflow-wrap: anywhere;
872
+ cursor: pointer;
873
+ }
874
+ .ck-reply:hover {
875
+ border-color: var(--ck-accent);
876
+ }
877
+ .ck-reply:disabled {
878
+ opacity: 0.5;
879
+ cursor: default;
880
+ }
881
+
654
882
  .ck-spin {
655
883
  animation: ck-spin 0.9s linear infinite;
656
884
  }
@@ -687,3 +915,110 @@
687
915
  animation: none;
688
916
  }
689
917
  }
918
+
919
+ /* attach sheet: Camera / Photos / Files, from the bottom edge on a phone —
920
+ where the thumb already is after tapping the paperclip. A native <dialog>
921
+ in the top layer, so no panel around the composer can clip it. */
922
+ .ck-sheet {
923
+ width: 100%;
924
+ max-width: 32rem;
925
+ max-height: none;
926
+ margin: auto auto 0;
927
+ padding: 0;
928
+ border: 0;
929
+ background: transparent;
930
+ color: var(--ck-fg);
931
+ font-family: var(--ck-font);
932
+ }
933
+ .ck-sheet::backdrop {
934
+ background: color-mix(in srgb, #000 40%, transparent);
935
+ }
936
+ .ck-sheet[open] .ck-sheet-panel {
937
+ animation: ck-rise 0.18s ease-out;
938
+ }
939
+ .ck-sheet-panel {
940
+ padding: 0.75rem 1rem calc(1rem + env(safe-area-inset-bottom, 0px));
941
+ border: 1px solid var(--ck-line);
942
+ border-bottom: 0;
943
+ border-radius: var(--ck-radius) var(--ck-radius) 0 0;
944
+ background: var(--ck-bg);
945
+ }
946
+ .ck-sheet-head {
947
+ display: flex;
948
+ align-items: center;
949
+ justify-content: space-between;
950
+ gap: 0.5rem;
951
+ margin-bottom: 0.75rem;
952
+ }
953
+ .ck-sheet-title {
954
+ font-size: 1rem;
955
+ font-weight: 600;
956
+ }
957
+ .ck-sheet-close {
958
+ border-color: transparent;
959
+ background: transparent;
960
+ }
961
+ .ck-attach-sources {
962
+ display: grid;
963
+ grid-auto-flow: column;
964
+ grid-auto-columns: minmax(0, 1fr);
965
+ gap: 0.5rem;
966
+ }
967
+ .ck-attach-source {
968
+ display: flex;
969
+ flex-direction: column;
970
+ align-items: center;
971
+ justify-content: center;
972
+ gap: 0.375rem;
973
+ min-height: 5rem;
974
+ padding: 0.75rem 0.25rem;
975
+ border: 1px solid var(--ck-line);
976
+ border-radius: var(--ck-radius-sm);
977
+ background: var(--ck-raised);
978
+ color: var(--ck-fg);
979
+ font: inherit;
980
+ font-size: 0.9375rem;
981
+ cursor: pointer;
982
+ }
983
+ .ck-attach-source > svg {
984
+ font-size: 1.375rem;
985
+ color: var(--ck-fg-2);
986
+ }
987
+ .ck-attach-source:hover {
988
+ border-color: var(--ck-line-2);
989
+ }
990
+ .ck-attach-source:focus-visible,
991
+ .ck-sheet-close:focus-visible {
992
+ outline: 2px solid var(--ck-accent);
993
+ outline-offset: 2px;
994
+ }
995
+ .ck-attach-trigger {
996
+ display: inline-flex;
997
+ align-items: center;
998
+ justify-content: center;
999
+ gap: 0.375rem;
1000
+ min-height: var(--ck-target);
1001
+ padding: 0 1rem;
1002
+ border: 1px solid var(--ck-line);
1003
+ border-radius: 999px;
1004
+ background: var(--ck-raised);
1005
+ color: var(--ck-fg);
1006
+ font: inherit;
1007
+ font-size: 0.9375rem;
1008
+ cursor: pointer;
1009
+ }
1010
+ .ck-attach-trigger:disabled {
1011
+ cursor: not-allowed;
1012
+ opacity: 0.45;
1013
+ }
1014
+ @keyframes ck-rise {
1015
+ from {
1016
+ transform: translateY(1.5rem);
1017
+ opacity: 0;
1018
+ }
1019
+ }
1020
+ @media (prefers-reduced-motion: reduce) {
1021
+ .ck-sheet[open] .ck-sheet-panel {
1022
+ animation: none;
1023
+ }
1024
+ }