@mrrocket/sdk 0.0.0-stage → 0.1.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/styles.js ADDED
@@ -0,0 +1,147 @@
1
+ "use client";
2
+
3
+ // src/styles.ts
4
+ import { useEffect } from "react";
5
+ var STYLE_ID = "ckw-styles";
6
+ var CSS = `
7
+ .ckw-root{--ckw-accent:#3f76ff;--ckw-accent-ink:#fff;--ckw-bg:#fff;--ckw-bg2:#f7f7f8;--ckw-bg3:#eeeff1;--ckw-fg:#1c1d20;--ckw-fg2:#5d6068;--ckw-fg3:#8b8e96;--ckw-line:#e4e5e8;--ckw-danger:#d92d20;--ckw-shadow:0 8px 30px rgba(16,18,24,.16);
8
+ font-size:14px;line-height:1.45;font-family:inherit;color:var(--ckw-fg);box-sizing:border-box;-webkit-font-smoothing:antialiased}
9
+ .ckw-root[data-theme=dark]{--ckw-bg:#17181b;--ckw-bg2:#1f2024;--ckw-bg3:#292a30;--ckw-fg:#f1f2f4;--ckw-fg2:#b0b3ba;--ckw-fg3:#7e818a;--ckw-line:#2e2f35;--ckw-shadow:0 8px 30px rgba(0,0,0,.5)}
10
+ @media (prefers-color-scheme:dark){.ckw-root[data-theme=auto]{--ckw-bg:#17181b;--ckw-bg2:#1f2024;--ckw-bg3:#292a30;--ckw-fg:#f1f2f4;--ckw-fg2:#b0b3ba;--ckw-fg3:#7e818a;--ckw-line:#2e2f35;--ckw-shadow:0 8px 30px rgba(0,0,0,.5)}}
11
+ .ckw-root *,.ckw-root *::before,.ckw-root *::after{box-sizing:border-box}
12
+ .ckw-root button,.ckw-root input,.ckw-root select,.ckw-root textarea{font:inherit;color:inherit;margin:0}
13
+ .ckw-root button{cursor:pointer}
14
+ .ckw-root :focus-visible{outline:2px solid var(--ckw-accent);outline-offset:2px}
15
+ .ckw-board{width:100%;max-width:760px;margin:0 auto;background:var(--ckw-bg);min-width:0}
16
+ .ckw-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}
17
+ .ckw-title{font-size:20px;font-weight:600;margin:0;letter-spacing:-.01em}
18
+ .ckw-sub{color:var(--ckw-fg2);margin:2px 0 0;font-size:13px}
19
+ .ckw-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:32px;padding:0 12px;border-radius:8px;border:1px solid var(--ckw-line);background:var(--ckw-bg);font-weight:500;font-size:13px;white-space:nowrap}
20
+ .ckw-btn:hover{background:var(--ckw-bg2)}
21
+ .ckw-btn[disabled]{opacity:.55;cursor:not-allowed}
22
+ .ckw-btn-primary{background:var(--ckw-accent);border-color:var(--ckw-accent);color:var(--ckw-accent-ink)}
23
+ .ckw-btn-primary:hover{background:var(--ckw-accent);filter:brightness(1.08)}
24
+ .ckw-btn-ghost{border-color:transparent;background:transparent;color:var(--ckw-fg2)}
25
+ .ckw-icon{width:16px;height:16px;flex:none;display:block}
26
+ .ckw-tabs{display:flex;gap:4px;border-bottom:1px solid var(--ckw-line);margin-bottom:12px;overflow-x:auto}
27
+ .ckw-tab{background:none;border:0;border-bottom:2px solid transparent;padding:8px 10px;margin-bottom:-1px;color:var(--ckw-fg2);font-weight:500;font-size:13px;white-space:nowrap}
28
+ .ckw-tab[aria-selected=true]{color:var(--ckw-fg);border-bottom-color:var(--ckw-accent)}
29
+ .ckw-tools{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px}
30
+ .ckw-input,.ckw-select,.ckw-textarea{width:100%;border:1px solid var(--ckw-line);border-radius:8px;background:var(--ckw-bg);padding:7px 10px;font-size:14px;min-height:34px}
31
+ .ckw-input::placeholder,.ckw-textarea::placeholder{color:var(--ckw-fg3)}
32
+ .ckw-textarea{resize:vertical;min-height:72px}
33
+ .ckw-search{flex:1 1 160px;min-width:0}
34
+ .ckw-seg{display:inline-flex;border:1px solid var(--ckw-line);border-radius:8px;overflow:hidden}
35
+ .ckw-seg button{border:0;background:var(--ckw-bg);padding:0 10px;height:32px;font-size:13px;color:var(--ckw-fg2)}
36
+ .ckw-seg button[aria-pressed=true]{background:var(--ckw-bg3);color:var(--ckw-fg);font-weight:500}
37
+ .ckw-chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}
38
+ .ckw-chip{border:1px solid var(--ckw-line);background:var(--ckw-bg);border-radius:999px;padding:3px 10px;font-size:12px;color:var(--ckw-fg2)}
39
+ .ckw-chip[aria-pressed=true]{background:var(--ckw-fg);color:var(--ckw-bg);border-color:var(--ckw-fg)}
40
+ .ckw-list{list-style:none;margin:0;padding:0;border:1px solid var(--ckw-line);border-radius:8px;overflow:hidden}
41
+ .ckw-list>li+li{border-top:1px solid var(--ckw-line)}
42
+ .ckw-row{display:flex;gap:12px;align-items:flex-start;padding:10px 12px;background:var(--ckw-bg)}
43
+ .ckw-row:hover{background:var(--ckw-bg2)}
44
+ .ckw-vote{flex:none;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0;width:44px;min-height:48px;border:1px solid var(--ckw-line);border-radius:8px;background:var(--ckw-bg);font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;padding:4px 0;color:var(--ckw-fg2)}
45
+ .ckw-vote:hover{border-color:var(--ckw-accent);color:var(--ckw-accent)}
46
+ .ckw-vote[aria-pressed=true]{background:var(--ckw-accent);border-color:var(--ckw-accent);color:var(--ckw-accent-ink)}
47
+ .ckw-vote-sm{width:auto;min-height:0;flex-direction:row;gap:4px;padding:2px 8px}
48
+ .ckw-main{flex:1;min-width:0;text-align:left}
49
+ .ckw-link{display:block;width:100%;text-align:left;background:none;border:0;padding:0;font-weight:600;font-size:14px;color:var(--ckw-fg);overflow-wrap:anywhere}
50
+ .ckw-link:hover{color:var(--ckw-accent)}
51
+ .ckw-excerpt{margin:2px 0 0;color:var(--ckw-fg2);font-size:13px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere}
52
+ .ckw-meta{display:flex;gap:8px;align-items:center;margin-top:6px;color:var(--ckw-fg3);font-size:12px;flex-wrap:wrap}
53
+ .ckw-pill{display:inline-flex;align-items:center;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600;background:var(--ckw-bg3);color:var(--ckw-fg2)}
54
+ .ckw-pill[data-s=planned]{background:rgba(63,118,255,.14);color:#3f76ff}
55
+ .ckw-pill[data-s=progress]{background:rgba(245,158,11,.16);color:#b45309}
56
+ .ckw-pill[data-s=shipped]{background:rgba(22,163,74,.15);color:#15803d}
57
+ .ckw-root[data-theme=dark] .ckw-pill[data-s=progress]{color:#fbbf24}
58
+ .ckw-root[data-theme=dark] .ckw-pill[data-s=shipped]{color:#4ade80}
59
+ @media (prefers-color-scheme:dark){.ckw-root[data-theme=auto] .ckw-pill[data-s=progress]{color:#fbbf24}.ckw-root[data-theme=auto] .ckw-pill[data-s=shipped]{color:#4ade80}}
60
+ .ckw-stat{display:inline-flex;align-items:center;gap:4px}
61
+ .ckw-empty{border:1px dashed var(--ckw-line);border-radius:8px;padding:28px 16px;text-align:center;color:var(--ckw-fg2)}
62
+ .ckw-error{border:1px solid color-mix(in srgb,var(--ckw-danger) 40%,transparent);background:color-mix(in srgb,var(--ckw-danger) 8%,transparent);color:var(--ckw-danger);border-radius:8px;padding:8px 10px;font-size:13px;display:flex;gap:8px;align-items:center;justify-content:space-between;margin-bottom:10px}
63
+ .ckw-skel{height:68px;border-radius:8px;background:var(--ckw-bg2);animation:ckw-sh 1.2s ease-in-out infinite;margin-bottom:6px}
64
+ @keyframes ckw-sh{50%{opacity:.5}}
65
+ @media (prefers-reduced-motion:reduce){.ckw-skel{animation:none}.ckw-root *{transition:none!important}}
66
+ .ckw-form{display:flex;flex-direction:column;gap:10px;border:1px solid var(--ckw-line);border-radius:8px;padding:12px;margin-bottom:12px;background:var(--ckw-bg)}
67
+ .ckw-field{display:flex;flex-direction:column;gap:4px}
68
+ .ckw-label{font-size:12px;font-weight:600;color:var(--ckw-fg2)}
69
+ .ckw-actions{display:flex;gap:8px;justify-content:flex-end}
70
+ .ckw-dupes{border:1px solid var(--ckw-line);border-radius:8px;background:var(--ckw-bg2);padding:8px 10px;font-size:13px}
71
+ .ckw-dupes p{margin:0 0 4px;color:var(--ckw-fg2);font-size:12px}
72
+ .ckw-dupe{display:flex;gap:8px;align-items:center;justify-content:space-between;padding:3px 0}
73
+ .ckw-dupe span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
74
+ .ckw-cols{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}
75
+ @media (max-width:640px){.ckw-cols{grid-template-columns:1fr}}
76
+ .ckw-col h3{font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--ckw-fg2);margin:0 0 8px;display:flex;gap:6px;align-items:center}
77
+ .ckw-card{border:1px solid var(--ckw-line);border-radius:8px;padding:10px;margin-bottom:8px;background:var(--ckw-bg)}
78
+ .ckw-card .ckw-link{margin-bottom:6px}
79
+ .ckw-entry{padding:12px 0;border-bottom:1px solid var(--ckw-line)}
80
+ .ckw-entry time{font-size:12px;color:var(--ckw-fg3)}
81
+ .ckw-entry h3{font-size:15px;margin:2px 0 4px}
82
+ .ckw-entry p{margin:0;color:var(--ckw-fg2);white-space:pre-wrap;overflow-wrap:anywhere}
83
+ .ckw-back{margin-bottom:8px;padding-left:4px}
84
+ .ckw-detail-head{display:flex;gap:12px;align-items:flex-start}
85
+ .ckw-detail-head h2{margin:0 0 4px;font-size:18px;overflow-wrap:anywhere}
86
+ .ckw-body{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--ckw-fg2);margin:12px 0}
87
+ .ckw-h{font-size:13px;font-weight:600;margin:16px 0 8px}
88
+ .ckw-comment{padding:8px 0;border-top:1px solid var(--ckw-line)}
89
+ .ckw-comment b{font-size:13px}
90
+ .ckw-comment p{margin:2px 0 0;white-space:pre-wrap;overflow-wrap:anywhere}
91
+ .ckw-team{margin-left:6px;background:var(--ckw-accent);color:var(--ckw-accent-ink);border-radius:4px;padding:0 5px;font-size:10px;font-weight:700;text-transform:uppercase;vertical-align:1px}
92
+ .ckw-time{color:var(--ckw-fg3);font-size:12px;margin-left:6px}
93
+ .ckw-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
94
+ /* Floating widget */
95
+ .ckw-fab{position:fixed;z-index:2147483000;bottom:20px;display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 16px;border-radius:999px;border:0;background:var(--ckw-accent);color:var(--ckw-accent-ink);font-weight:600;font-size:14px;box-shadow:var(--ckw-shadow)}
96
+ .ckw-fab[data-pos=bottom-right],.ckw-pop[data-pos=bottom-right]{right:20px}
97
+ .ckw-fab[data-pos=bottom-left],.ckw-pop[data-pos=bottom-left]{left:20px}
98
+ .ckw-pop{position:fixed;z-index:2147483001;bottom:70px;width:380px;max-width:calc(100vw - 24px);max-height:min(560px,calc(100vh - 90px));display:flex;flex-direction:column;background:var(--ckw-bg);border:1px solid var(--ckw-line);border-radius:12px;box-shadow:var(--ckw-shadow);overflow:hidden}
99
+ .ckw-pop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--ckw-line);font-weight:600}
100
+ .ckw-pop-body{padding:12px;overflow-y:auto;flex:1}
101
+ .ckw-pop-foot{padding:8px 12px;border-top:1px solid var(--ckw-line);display:flex;justify-content:space-between;align-items:center;font-size:13px}
102
+ .ckw-pop-foot a{color:var(--ckw-accent);text-decoration:none;font-weight:500}
103
+ .ckw-pop .ckw-list{border:0;border-radius:0}
104
+ .ckw-pop .ckw-row{padding:8px 0}
105
+ .ckw-pop .ckw-form{border:0;padding:0;margin:12px 0 0}
106
+ @media (max-width:520px){.ckw-pop{left:0!important;right:0!important;bottom:0;width:100%;max-width:100%;border-radius:12px 12px 0 0;max-height:85vh}.ckw-fab{bottom:14px}.ckw-fab[data-pos=bottom-right]{right:14px}.ckw-fab[data-pos=bottom-left]{left:14px}}
107
+ /* Survey */
108
+ .ckw-survey{position:fixed;z-index:2147483000;left:20px;bottom:20px;width:360px;max-width:calc(100vw - 24px);background:var(--ckw-bg);border:1px solid var(--ckw-line);border-radius:12px;box-shadow:var(--ckw-shadow);padding:14px;animation:ckw-in .2s ease-out}
109
+ @keyframes ckw-in{from{opacity:0;transform:translateY(8px)}}
110
+ @media (max-width:520px){.ckw-survey{left:0;right:0;bottom:0;width:100%;max-width:100%;border-radius:12px 12px 0 0}}
111
+ .ckw-sv-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
112
+ .ckw-dots{display:flex;gap:4px}
113
+ .ckw-dot{width:6px;height:6px;border-radius:50%;background:var(--ckw-bg3)}
114
+ .ckw-dot[data-on=true]{background:var(--ckw-accent)}
115
+ .ckw-q{font-weight:600;font-size:15px;margin:0 0 2px;border:0;padding:0}
116
+ .ckw-root .ckw-q:focus-visible{outline:none}
117
+ .ckw-qd{color:var(--ckw-fg2);font-size:13px;margin:0 0 10px}
118
+ .ckw-scale{display:flex;gap:4px;flex-wrap:wrap}
119
+ .ckw-num{flex:1 1 0;min-width:26px;height:34px;border:1px solid var(--ckw-line);background:var(--ckw-bg);border-radius:8px;font-size:13px;font-weight:600;padding:0}
120
+ .ckw-num:hover{border-color:var(--ckw-accent)}
121
+ .ckw-num[aria-pressed=true]{background:var(--ckw-accent);color:var(--ckw-accent-ink);border-color:var(--ckw-accent)}
122
+ .ckw-star{background:none;border:0;padding:2px;color:var(--ckw-bg3)}
123
+ .ckw-star[data-on=true]{color:#f5a524}
124
+ .ckw-star svg{width:28px;height:28px}
125
+ .ckw-lh{display:flex;justify-content:space-between;color:var(--ckw-fg3);font-size:11px;margin-top:4px}
126
+ .ckw-opts{display:flex;flex-direction:column;gap:6px}
127
+ .ckw-opt{display:flex;gap:8px;align-items:center;border:1px solid var(--ckw-line);border-radius:8px;padding:7px 10px;background:var(--ckw-bg);cursor:pointer}
128
+ .ckw-opt:has(input:checked){border-color:var(--ckw-accent);background:color-mix(in srgb,var(--ckw-accent) 8%,transparent)}
129
+ .ckw-opt input{accent-color:var(--ckw-accent)}
130
+ .ckw-sv-foot{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:12px}
131
+ .ckw-thanks{text-align:center;padding:14px 0;display:flex;flex-direction:column;align-items:center;gap:8px}
132
+ .ckw-tick{width:32px;height:32px;border-radius:50%;background:var(--ckw-accent);color:var(--ckw-accent-ink);display:flex;align-items:center;justify-content:center}
133
+ `;
134
+ var useStyles = () => {
135
+ useEffect(() => {
136
+ if (document.getElementById(STYLE_ID)) return;
137
+ const el = document.createElement("style");
138
+ el.id = STYLE_ID;
139
+ el.textContent = CSS;
140
+ document.head.appendChild(el);
141
+ }, []);
142
+ };
143
+ export {
144
+ CSS,
145
+ useStyles
146
+ };
147
+ //# sourceMappingURL=styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/styles.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect } from \"react\";\n\nconst STYLE_ID = \"ckw-styles\";\n\n/** The one stylesheet for every Cockpit widget. Everything is scoped under `.ckw-root` and prefixed `ckw-`. */\nexport const CSS = `\n.ckw-root{--ckw-accent:#3f76ff;--ckw-accent-ink:#fff;--ckw-bg:#fff;--ckw-bg2:#f7f7f8;--ckw-bg3:#eeeff1;--ckw-fg:#1c1d20;--ckw-fg2:#5d6068;--ckw-fg3:#8b8e96;--ckw-line:#e4e5e8;--ckw-danger:#d92d20;--ckw-shadow:0 8px 30px rgba(16,18,24,.16);\nfont-size:14px;line-height:1.45;font-family:inherit;color:var(--ckw-fg);box-sizing:border-box;-webkit-font-smoothing:antialiased}\n.ckw-root[data-theme=dark]{--ckw-bg:#17181b;--ckw-bg2:#1f2024;--ckw-bg3:#292a30;--ckw-fg:#f1f2f4;--ckw-fg2:#b0b3ba;--ckw-fg3:#7e818a;--ckw-line:#2e2f35;--ckw-shadow:0 8px 30px rgba(0,0,0,.5)}\n@media (prefers-color-scheme:dark){.ckw-root[data-theme=auto]{--ckw-bg:#17181b;--ckw-bg2:#1f2024;--ckw-bg3:#292a30;--ckw-fg:#f1f2f4;--ckw-fg2:#b0b3ba;--ckw-fg3:#7e818a;--ckw-line:#2e2f35;--ckw-shadow:0 8px 30px rgba(0,0,0,.5)}}\n.ckw-root *,.ckw-root *::before,.ckw-root *::after{box-sizing:border-box}\n.ckw-root button,.ckw-root input,.ckw-root select,.ckw-root textarea{font:inherit;color:inherit;margin:0}\n.ckw-root button{cursor:pointer}\n.ckw-root :focus-visible{outline:2px solid var(--ckw-accent);outline-offset:2px}\n.ckw-board{width:100%;max-width:760px;margin:0 auto;background:var(--ckw-bg);min-width:0}\n.ckw-head{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap;margin-bottom:12px}\n.ckw-title{font-size:20px;font-weight:600;margin:0;letter-spacing:-.01em}\n.ckw-sub{color:var(--ckw-fg2);margin:2px 0 0;font-size:13px}\n.ckw-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:32px;padding:0 12px;border-radius:8px;border:1px solid var(--ckw-line);background:var(--ckw-bg);font-weight:500;font-size:13px;white-space:nowrap}\n.ckw-btn:hover{background:var(--ckw-bg2)}\n.ckw-btn[disabled]{opacity:.55;cursor:not-allowed}\n.ckw-btn-primary{background:var(--ckw-accent);border-color:var(--ckw-accent);color:var(--ckw-accent-ink)}\n.ckw-btn-primary:hover{background:var(--ckw-accent);filter:brightness(1.08)}\n.ckw-btn-ghost{border-color:transparent;background:transparent;color:var(--ckw-fg2)}\n.ckw-icon{width:16px;height:16px;flex:none;display:block}\n.ckw-tabs{display:flex;gap:4px;border-bottom:1px solid var(--ckw-line);margin-bottom:12px;overflow-x:auto}\n.ckw-tab{background:none;border:0;border-bottom:2px solid transparent;padding:8px 10px;margin-bottom:-1px;color:var(--ckw-fg2);font-weight:500;font-size:13px;white-space:nowrap}\n.ckw-tab[aria-selected=true]{color:var(--ckw-fg);border-bottom-color:var(--ckw-accent)}\n.ckw-tools{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:10px}\n.ckw-input,.ckw-select,.ckw-textarea{width:100%;border:1px solid var(--ckw-line);border-radius:8px;background:var(--ckw-bg);padding:7px 10px;font-size:14px;min-height:34px}\n.ckw-input::placeholder,.ckw-textarea::placeholder{color:var(--ckw-fg3)}\n.ckw-textarea{resize:vertical;min-height:72px}\n.ckw-search{flex:1 1 160px;min-width:0}\n.ckw-seg{display:inline-flex;border:1px solid var(--ckw-line);border-radius:8px;overflow:hidden}\n.ckw-seg button{border:0;background:var(--ckw-bg);padding:0 10px;height:32px;font-size:13px;color:var(--ckw-fg2)}\n.ckw-seg button[aria-pressed=true]{background:var(--ckw-bg3);color:var(--ckw-fg);font-weight:500}\n.ckw-chips{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px}\n.ckw-chip{border:1px solid var(--ckw-line);background:var(--ckw-bg);border-radius:999px;padding:3px 10px;font-size:12px;color:var(--ckw-fg2)}\n.ckw-chip[aria-pressed=true]{background:var(--ckw-fg);color:var(--ckw-bg);border-color:var(--ckw-fg)}\n.ckw-list{list-style:none;margin:0;padding:0;border:1px solid var(--ckw-line);border-radius:8px;overflow:hidden}\n.ckw-list>li+li{border-top:1px solid var(--ckw-line)}\n.ckw-row{display:flex;gap:12px;align-items:flex-start;padding:10px 12px;background:var(--ckw-bg)}\n.ckw-row:hover{background:var(--ckw-bg2)}\n.ckw-vote{flex:none;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0;width:44px;min-height:48px;border:1px solid var(--ckw-line);border-radius:8px;background:var(--ckw-bg);font-size:13px;font-weight:600;font-variant-numeric:tabular-nums;padding:4px 0;color:var(--ckw-fg2)}\n.ckw-vote:hover{border-color:var(--ckw-accent);color:var(--ckw-accent)}\n.ckw-vote[aria-pressed=true]{background:var(--ckw-accent);border-color:var(--ckw-accent);color:var(--ckw-accent-ink)}\n.ckw-vote-sm{width:auto;min-height:0;flex-direction:row;gap:4px;padding:2px 8px}\n.ckw-main{flex:1;min-width:0;text-align:left}\n.ckw-link{display:block;width:100%;text-align:left;background:none;border:0;padding:0;font-weight:600;font-size:14px;color:var(--ckw-fg);overflow-wrap:anywhere}\n.ckw-link:hover{color:var(--ckw-accent)}\n.ckw-excerpt{margin:2px 0 0;color:var(--ckw-fg2);font-size:13px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere}\n.ckw-meta{display:flex;gap:8px;align-items:center;margin-top:6px;color:var(--ckw-fg3);font-size:12px;flex-wrap:wrap}\n.ckw-pill{display:inline-flex;align-items:center;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600;background:var(--ckw-bg3);color:var(--ckw-fg2)}\n.ckw-pill[data-s=planned]{background:rgba(63,118,255,.14);color:#3f76ff}\n.ckw-pill[data-s=progress]{background:rgba(245,158,11,.16);color:#b45309}\n.ckw-pill[data-s=shipped]{background:rgba(22,163,74,.15);color:#15803d}\n.ckw-root[data-theme=dark] .ckw-pill[data-s=progress]{color:#fbbf24}\n.ckw-root[data-theme=dark] .ckw-pill[data-s=shipped]{color:#4ade80}\n@media (prefers-color-scheme:dark){.ckw-root[data-theme=auto] .ckw-pill[data-s=progress]{color:#fbbf24}.ckw-root[data-theme=auto] .ckw-pill[data-s=shipped]{color:#4ade80}}\n.ckw-stat{display:inline-flex;align-items:center;gap:4px}\n.ckw-empty{border:1px dashed var(--ckw-line);border-radius:8px;padding:28px 16px;text-align:center;color:var(--ckw-fg2)}\n.ckw-error{border:1px solid color-mix(in srgb,var(--ckw-danger) 40%,transparent);background:color-mix(in srgb,var(--ckw-danger) 8%,transparent);color:var(--ckw-danger);border-radius:8px;padding:8px 10px;font-size:13px;display:flex;gap:8px;align-items:center;justify-content:space-between;margin-bottom:10px}\n.ckw-skel{height:68px;border-radius:8px;background:var(--ckw-bg2);animation:ckw-sh 1.2s ease-in-out infinite;margin-bottom:6px}\n@keyframes ckw-sh{50%{opacity:.5}}\n@media (prefers-reduced-motion:reduce){.ckw-skel{animation:none}.ckw-root *{transition:none!important}}\n.ckw-form{display:flex;flex-direction:column;gap:10px;border:1px solid var(--ckw-line);border-radius:8px;padding:12px;margin-bottom:12px;background:var(--ckw-bg)}\n.ckw-field{display:flex;flex-direction:column;gap:4px}\n.ckw-label{font-size:12px;font-weight:600;color:var(--ckw-fg2)}\n.ckw-actions{display:flex;gap:8px;justify-content:flex-end}\n.ckw-dupes{border:1px solid var(--ckw-line);border-radius:8px;background:var(--ckw-bg2);padding:8px 10px;font-size:13px}\n.ckw-dupes p{margin:0 0 4px;color:var(--ckw-fg2);font-size:12px}\n.ckw-dupe{display:flex;gap:8px;align-items:center;justify-content:space-between;padding:3px 0}\n.ckw-dupe span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.ckw-cols{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}\n@media (max-width:640px){.ckw-cols{grid-template-columns:1fr}}\n.ckw-col h3{font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--ckw-fg2);margin:0 0 8px;display:flex;gap:6px;align-items:center}\n.ckw-card{border:1px solid var(--ckw-line);border-radius:8px;padding:10px;margin-bottom:8px;background:var(--ckw-bg)}\n.ckw-card .ckw-link{margin-bottom:6px}\n.ckw-entry{padding:12px 0;border-bottom:1px solid var(--ckw-line)}\n.ckw-entry time{font-size:12px;color:var(--ckw-fg3)}\n.ckw-entry h3{font-size:15px;margin:2px 0 4px}\n.ckw-entry p{margin:0;color:var(--ckw-fg2);white-space:pre-wrap;overflow-wrap:anywhere}\n.ckw-back{margin-bottom:8px;padding-left:4px}\n.ckw-detail-head{display:flex;gap:12px;align-items:flex-start}\n.ckw-detail-head h2{margin:0 0 4px;font-size:18px;overflow-wrap:anywhere}\n.ckw-body{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--ckw-fg2);margin:12px 0}\n.ckw-h{font-size:13px;font-weight:600;margin:16px 0 8px}\n.ckw-comment{padding:8px 0;border-top:1px solid var(--ckw-line)}\n.ckw-comment b{font-size:13px}\n.ckw-comment p{margin:2px 0 0;white-space:pre-wrap;overflow-wrap:anywhere}\n.ckw-team{margin-left:6px;background:var(--ckw-accent);color:var(--ckw-accent-ink);border-radius:4px;padding:0 5px;font-size:10px;font-weight:700;text-transform:uppercase;vertical-align:1px}\n.ckw-time{color:var(--ckw-fg3);font-size:12px;margin-left:6px}\n.ckw-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}\n/* Floating widget */\n.ckw-fab{position:fixed;z-index:2147483000;bottom:20px;display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 16px;border-radius:999px;border:0;background:var(--ckw-accent);color:var(--ckw-accent-ink);font-weight:600;font-size:14px;box-shadow:var(--ckw-shadow)}\n.ckw-fab[data-pos=bottom-right],.ckw-pop[data-pos=bottom-right]{right:20px}\n.ckw-fab[data-pos=bottom-left],.ckw-pop[data-pos=bottom-left]{left:20px}\n.ckw-pop{position:fixed;z-index:2147483001;bottom:70px;width:380px;max-width:calc(100vw - 24px);max-height:min(560px,calc(100vh - 90px));display:flex;flex-direction:column;background:var(--ckw-bg);border:1px solid var(--ckw-line);border-radius:12px;box-shadow:var(--ckw-shadow);overflow:hidden}\n.ckw-pop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--ckw-line);font-weight:600}\n.ckw-pop-body{padding:12px;overflow-y:auto;flex:1}\n.ckw-pop-foot{padding:8px 12px;border-top:1px solid var(--ckw-line);display:flex;justify-content:space-between;align-items:center;font-size:13px}\n.ckw-pop-foot a{color:var(--ckw-accent);text-decoration:none;font-weight:500}\n.ckw-pop .ckw-list{border:0;border-radius:0}\n.ckw-pop .ckw-row{padding:8px 0}\n.ckw-pop .ckw-form{border:0;padding:0;margin:12px 0 0}\n@media (max-width:520px){.ckw-pop{left:0!important;right:0!important;bottom:0;width:100%;max-width:100%;border-radius:12px 12px 0 0;max-height:85vh}.ckw-fab{bottom:14px}.ckw-fab[data-pos=bottom-right]{right:14px}.ckw-fab[data-pos=bottom-left]{left:14px}}\n/* Survey */\n.ckw-survey{position:fixed;z-index:2147483000;left:20px;bottom:20px;width:360px;max-width:calc(100vw - 24px);background:var(--ckw-bg);border:1px solid var(--ckw-line);border-radius:12px;box-shadow:var(--ckw-shadow);padding:14px;animation:ckw-in .2s ease-out}\n@keyframes ckw-in{from{opacity:0;transform:translateY(8px)}}\n@media (max-width:520px){.ckw-survey{left:0;right:0;bottom:0;width:100%;max-width:100%;border-radius:12px 12px 0 0}}\n.ckw-sv-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}\n.ckw-dots{display:flex;gap:4px}\n.ckw-dot{width:6px;height:6px;border-radius:50%;background:var(--ckw-bg3)}\n.ckw-dot[data-on=true]{background:var(--ckw-accent)}\n.ckw-q{font-weight:600;font-size:15px;margin:0 0 2px;border:0;padding:0}\n.ckw-root .ckw-q:focus-visible{outline:none}\n.ckw-qd{color:var(--ckw-fg2);font-size:13px;margin:0 0 10px}\n.ckw-scale{display:flex;gap:4px;flex-wrap:wrap}\n.ckw-num{flex:1 1 0;min-width:26px;height:34px;border:1px solid var(--ckw-line);background:var(--ckw-bg);border-radius:8px;font-size:13px;font-weight:600;padding:0}\n.ckw-num:hover{border-color:var(--ckw-accent)}\n.ckw-num[aria-pressed=true]{background:var(--ckw-accent);color:var(--ckw-accent-ink);border-color:var(--ckw-accent)}\n.ckw-star{background:none;border:0;padding:2px;color:var(--ckw-bg3)}\n.ckw-star[data-on=true]{color:#f5a524}\n.ckw-star svg{width:28px;height:28px}\n.ckw-lh{display:flex;justify-content:space-between;color:var(--ckw-fg3);font-size:11px;margin-top:4px}\n.ckw-opts{display:flex;flex-direction:column;gap:6px}\n.ckw-opt{display:flex;gap:8px;align-items:center;border:1px solid var(--ckw-line);border-radius:8px;padding:7px 10px;background:var(--ckw-bg);cursor:pointer}\n.ckw-opt:has(input:checked){border-color:var(--ckw-accent);background:color-mix(in srgb,var(--ckw-accent) 8%,transparent)}\n.ckw-opt input{accent-color:var(--ckw-accent)}\n.ckw-sv-foot{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:12px}\n.ckw-thanks{text-align:center;padding:14px 0;display:flex;flex-direction:column;align-items:center;gap:8px}\n.ckw-tick{width:32px;height:32px;border-radius:50%;background:var(--ckw-accent);color:var(--ckw-accent-ink);display:flex;align-items:center;justify-content:center}\n`;\n\n/** Puts the stylesheet into the page once, however many widgets mount. */\nexport const useStyles = () => {\n useEffect(() => {\n if (document.getElementById(STYLE_ID)) return;\n const el = document.createElement(\"style\");\n el.id = STYLE_ID;\n el.textContent = CSS;\n document.head.appendChild(el);\n }, []);\n};\n"],"mappings":";;;AAEA,SAAS,iBAAiB;AAE1B,IAAM,WAAW;AAGV,IAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkIZ,IAAM,YAAY,MAAM;AAC3B,YAAU,MAAM;AACZ,QAAI,SAAS,eAAe,QAAQ,EAAG;AACvC,UAAM,KAAK,SAAS,cAAc,OAAO;AACzC,OAAG,KAAK;AACR,OAAG,cAAc;AACjB,aAAS,KAAK,YAAY,EAAE;AAAA,EAChC,GAAG,CAAC,CAAC;AACT;","names":[]}
@@ -0,0 +1,4 @@
1
+ /** Consent for storage was withdrawn: move the stored dismissals into memory for this page load and delete them. */
2
+ export declare const forgetStoredDismissals: () => void;
3
+ /** Asks the visitor the survey Cockpit picked for this page. Mounted by CockpitProvider unless `surveys={false}`. */
4
+ export declare const Survey: () => import("react").JSX.Element | null;
package/dist/survey.js ADDED
@@ -0,0 +1,201 @@
1
+ "use client";
2
+
3
+ // src/survey.tsx
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { useApi } from "./api.js";
6
+ import { useCtx } from "./context.js";
7
+ import { Check, Cross, Root, Star } from "./ui.js";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ var DISMISS_PREFIX = "ck_survey_";
10
+ var dismissed = /* @__PURE__ */ new Set();
11
+ var remember = (id, persist) => {
12
+ dismissed.add(id);
13
+ if (!persist) return;
14
+ try {
15
+ localStorage.setItem(`${DISMISS_PREFIX}${id}`, "1");
16
+ } catch {
17
+ }
18
+ };
19
+ var wasDismissed = (id, persist) => {
20
+ if (dismissed.has(id)) return true;
21
+ if (!persist) return false;
22
+ try {
23
+ return !!localStorage.getItem(`${DISMISS_PREFIX}${id}`);
24
+ } catch {
25
+ return false;
26
+ }
27
+ };
28
+ var forgetStoredDismissals = () => {
29
+ try {
30
+ for (let i = localStorage.length - 1; i >= 0; i--) {
31
+ const key = localStorage.key(i);
32
+ if (!key?.startsWith(DISMISS_PREFIX)) continue;
33
+ dismissed.add(key.slice(DISMISS_PREFIX.length));
34
+ localStorage.removeItem(key);
35
+ }
36
+ } catch {
37
+ }
38
+ };
39
+ var isAnswered = (a) => Array.isArray(a) ? a.length > 0 : typeof a === "number" ? true : !!a?.trim();
40
+ var Question = ({ q, value, onChange, onEnter }) => {
41
+ if (q.kind === "nps" || q.kind === "rating") {
42
+ const scale = q.kind === "nps" ? 11 : Math.min(Math.max(q.scale ?? 5, 2), 10);
43
+ const start = q.kind === "nps" ? 0 : 1;
44
+ const items = Array.from({ length: scale }, (_, i) => i + start);
45
+ const stars = q.kind === "rating" && scale <= 5;
46
+ return /* @__PURE__ */ jsxs("div", { children: [
47
+ /* @__PURE__ */ jsx("div", { className: "ckw-scale", role: "group", "aria-label": q.title, children: items.map(
48
+ (n) => stars ? /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-star", "data-on": typeof value === "number" && n <= value, "aria-pressed": value === n, "aria-label": `${n} of ${scale}`, onClick: () => onChange(n), children: /* @__PURE__ */ jsx(Star, {}) }, n) : /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-num", "aria-pressed": value === n, onClick: () => onChange(n), children: n }, n)
49
+ ) }),
50
+ (q.low || q.high || q.kind === "nps") && /* @__PURE__ */ jsxs("div", { className: "ckw-lh", children: [
51
+ /* @__PURE__ */ jsx("span", { children: q.low ?? (q.kind === "nps" ? "Not likely" : "") }),
52
+ /* @__PURE__ */ jsx("span", { children: q.high ?? (q.kind === "nps" ? "Very likely" : "") })
53
+ ] })
54
+ ] });
55
+ }
56
+ if (q.kind === "single" || q.kind === "multi") {
57
+ const multi = q.kind === "multi";
58
+ const picked = multi ? value ?? [] : [];
59
+ return /* @__PURE__ */ jsx("div", { className: "ckw-opts", role: multi ? "group" : "radiogroup", "aria-label": q.title, children: (q.options ?? []).map((o) => /* @__PURE__ */ jsxs("label", { className: "ckw-opt", children: [
60
+ /* @__PURE__ */ jsx(
61
+ "input",
62
+ {
63
+ type: multi ? "checkbox" : "radio",
64
+ name: q.key,
65
+ checked: multi ? picked.includes(o) : value === o,
66
+ onChange: () => onChange(multi ? picked.includes(o) ? picked.filter((x) => x !== o) : [...picked, o] : o)
67
+ }
68
+ ),
69
+ /* @__PURE__ */ jsx("span", { children: o })
70
+ ] }, o)) });
71
+ }
72
+ return /* @__PURE__ */ jsx(
73
+ "textarea",
74
+ {
75
+ className: "ckw-textarea",
76
+ "aria-label": q.title,
77
+ placeholder: q.placeholder ?? "Type your answer",
78
+ value: value ?? "",
79
+ maxLength: 2e3,
80
+ onChange: (e) => onChange(e.target.value),
81
+ onKeyDown: (e) => (e.metaKey || e.ctrlKey) && e.key === "Enter" && onEnter()
82
+ }
83
+ );
84
+ };
85
+ var Card = ({ survey, onClose }) => {
86
+ const api = useApi();
87
+ const persist = useCtx().effectiveConsent === "cookies";
88
+ const [step, setStep] = useState(0);
89
+ const [answers, setAnswers] = useState({});
90
+ const [done, setDone] = useState(false);
91
+ const [error, setError] = useState("");
92
+ const started = useRef(Date.now());
93
+ const finished = useRef(false);
94
+ const live = useRef(answers);
95
+ live.current = answers;
96
+ const heading = useRef(null);
97
+ const q = survey.questions[step];
98
+ const last = step === survey.questions.length - 1;
99
+ const has = isAnswered(answers[q?.key]);
100
+ useEffect(() => {
101
+ api.seen(survey.id).catch(() => void 0);
102
+ }, [api, survey.id]);
103
+ useEffect(() => {
104
+ heading.current?.focus();
105
+ }, [step]);
106
+ const finish = useCallback(
107
+ async (completed) => {
108
+ if (finished.current) return;
109
+ finished.current = true;
110
+ remember(survey.id, persist);
111
+ api.respond(survey.id, { answers: live.current, completed, seconds: Math.round((Date.now() - started.current) / 1e3) }).catch(() => void 0);
112
+ },
113
+ [api, survey.id, persist]
114
+ );
115
+ useEffect(() => {
116
+ if (!done) return;
117
+ const t = window.setTimeout(onClose, 2500);
118
+ return () => window.clearTimeout(t);
119
+ }, [done, onClose]);
120
+ const dismiss = useCallback(() => {
121
+ if (Object.values(live.current).some(isAnswered)) void finish(false);
122
+ else remember(survey.id, persist);
123
+ onClose();
124
+ }, [finish, onClose, survey.id, persist]);
125
+ useEffect(() => {
126
+ const h = (e) => e.key === "Escape" && !done && dismiss();
127
+ document.addEventListener("keydown", h);
128
+ return () => document.removeEventListener("keydown", h);
129
+ }, [dismiss, done]);
130
+ const next = async () => {
131
+ if (q.required && !has) {
132
+ setError("Please answer this question.");
133
+ return;
134
+ }
135
+ setError("");
136
+ if (!last) return setStep(step + 1);
137
+ await finish(true);
138
+ setDone(true);
139
+ };
140
+ if (done)
141
+ return /* @__PURE__ */ jsxs("div", { className: "ckw-thanks", role: "status", children: [
142
+ /* @__PURE__ */ jsx("span", { className: "ckw-tick", children: /* @__PURE__ */ jsx(Check, {}) }),
143
+ /* @__PURE__ */ jsx("b", { children: survey.thanks || "Thanks for your feedback." })
144
+ ] });
145
+ return /* @__PURE__ */ jsxs(
146
+ "form",
147
+ {
148
+ onSubmit: (e) => {
149
+ e.preventDefault();
150
+ void next();
151
+ },
152
+ children: [
153
+ /* @__PURE__ */ jsxs("div", { className: "ckw-sv-top", children: [
154
+ /* @__PURE__ */ jsx("div", { className: "ckw-dots", role: "img", "aria-label": `Question ${step + 1} of ${survey.questions.length}`, children: survey.questions.map((x, i) => /* @__PURE__ */ jsx("span", { className: "ckw-dot", "data-on": i <= step }, x.key)) }),
155
+ /* @__PURE__ */ jsx("button", { type: "button", className: "ckw-btn ckw-btn-ghost", onClick: dismiss, "aria-label": "Close survey", style: { height: 24, padding: "0 4px" }, children: /* @__PURE__ */ jsx(Cross, {}) })
156
+ ] }),
157
+ /* @__PURE__ */ jsxs("h3", { className: "ckw-q", tabIndex: -1, ref: heading, children: [
158
+ q.title,
159
+ !q.required && /* @__PURE__ */ jsx("span", { className: "ckw-time", children: " optional" })
160
+ ] }),
161
+ q.description && /* @__PURE__ */ jsx("p", { className: "ckw-qd", children: q.description }),
162
+ /* @__PURE__ */ jsx(Question, { q, value: answers[q.key], onChange: (v) => (setAnswers((a) => ({ ...a, [q.key]: v })), setError("")), onEnter: () => void next() }),
163
+ /* @__PURE__ */ jsx("div", { "aria-live": "polite", children: error && /* @__PURE__ */ jsx("p", { className: "ckw-qd", style: { color: "var(--ckw-danger)", margin: "8px 0 0" }, children: error }) }),
164
+ /* @__PURE__ */ jsxs("div", { className: "ckw-sv-foot", children: [
165
+ /* @__PURE__ */ jsx("span", {}),
166
+ /* @__PURE__ */ jsx("button", { type: "submit", className: "ckw-btn ckw-btn-primary", children: last ? "Send" : "Next" })
167
+ ] })
168
+ ]
169
+ }
170
+ );
171
+ };
172
+ var Survey = () => {
173
+ const { embedded, distinctId, effectiveConsent } = useCtx();
174
+ const api = useApi();
175
+ const [survey, setSurvey] = useState(null);
176
+ const off = embedded || effectiveConsent === "off" || distinctId === "anonymous";
177
+ const persist = effectiveConsent === "cookies";
178
+ useEffect(() => {
179
+ if (off) return;
180
+ const ctl = new AbortController();
181
+ const timer = window.setTimeout(() => {
182
+ api.survey(window.location.href, ctl.signal).then((r) => {
183
+ const s = r.survey;
184
+ if (!s || s.questions.length === 0 || wasDismissed(s.id, persist)) return;
185
+ setSurvey(s);
186
+ }).catch(() => void 0);
187
+ }, 3e3);
188
+ return () => {
189
+ window.clearTimeout(timer);
190
+ ctl.abort();
191
+ };
192
+ }, [api, off, persist]);
193
+ const close = useCallback(() => setSurvey(null), []);
194
+ if (!survey || off) return null;
195
+ return /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx("div", { className: "ckw-survey", role: "dialog", "aria-label": survey.name, children: /* @__PURE__ */ jsx(Card, { survey, onClose: close }, survey.id) }) });
196
+ };
197
+ export {
198
+ Survey,
199
+ forgetStoredDismissals
200
+ };
201
+ //# sourceMappingURL=survey.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/survey.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { type SurveyAnswer, type SurveyDefinition, type SurveyQuestion, useApi } from \"./api\";\nimport { useCtx } from \"./context\";\nimport { Check, Cross, Root, Star } from \"./ui\";\n\nconst DISMISS_PREFIX = \"ck_survey_\";\n/** Surveys dismissed or answered during this page load. Only \"cookies\" mode also keeps them in localStorage. */\nconst dismissed = new Set<string>();\n\nconst remember = (id: string, persist: boolean) => {\n dismissed.add(id);\n if (!persist) return;\n try {\n localStorage.setItem(`${DISMISS_PREFIX}${id}`, \"1\");\n } catch {\n // Private mode: the card just may show again next visit.\n }\n};\n\nconst wasDismissed = (id: string, persist: boolean) => {\n if (dismissed.has(id)) return true;\n if (!persist) return false;\n try {\n return !!localStorage.getItem(`${DISMISS_PREFIX}${id}`);\n } catch {\n return false;\n }\n};\n\n/** Consent for storage was withdrawn: move the stored dismissals into memory for this page load and delete them. */\nexport const forgetStoredDismissals = () => {\n try {\n for (let i = localStorage.length - 1; i >= 0; i--) {\n const key = localStorage.key(i);\n if (!key?.startsWith(DISMISS_PREFIX)) continue;\n dismissed.add(key.slice(DISMISS_PREFIX.length));\n localStorage.removeItem(key);\n }\n } catch {\n // No storage: nothing to delete.\n }\n};\n\nconst isAnswered = (a: SurveyAnswer | undefined) => (Array.isArray(a) ? a.length > 0 : typeof a === \"number\" ? true : !!a?.trim());\n\nconst Question = ({ q, value, onChange, onEnter }: { q: SurveyQuestion; value: SurveyAnswer | undefined; onChange: (v: SurveyAnswer) => void; onEnter: () => void }) => {\n if (q.kind === \"nps\" || q.kind === \"rating\") {\n const scale = q.kind === \"nps\" ? 11 : Math.min(Math.max(q.scale ?? 5, 2), 10);\n const start = q.kind === \"nps\" ? 0 : 1;\n const items = Array.from({ length: scale }, (_, i) => i + start);\n const stars = q.kind === \"rating\" && scale <= 5;\n return (\n <div>\n <div className=\"ckw-scale\" role=\"group\" aria-label={q.title}>\n {items.map((n) =>\n stars ? (\n <button key={n} type=\"button\" className=\"ckw-star\" data-on={typeof value === \"number\" && n <= value} aria-pressed={value === n} aria-label={`${n} of ${scale}`} onClick={() => onChange(n)}>\n <Star />\n </button>\n ) : (\n <button key={n} type=\"button\" className=\"ckw-num\" aria-pressed={value === n} onClick={() => onChange(n)}>\n {n}\n </button>\n ),\n )}\n </div>\n {(q.low || q.high || q.kind === \"nps\") && (\n <div className=\"ckw-lh\">\n <span>{q.low ?? (q.kind === \"nps\" ? \"Not likely\" : \"\")}</span>\n <span>{q.high ?? (q.kind === \"nps\" ? \"Very likely\" : \"\")}</span>\n </div>\n )}\n </div>\n );\n }\n if (q.kind === \"single\" || q.kind === \"multi\") {\n const multi = q.kind === \"multi\";\n const picked = multi ? ((value as string[] | undefined) ?? []) : [];\n return (\n <div className=\"ckw-opts\" role={multi ? \"group\" : \"radiogroup\"} aria-label={q.title}>\n {(q.options ?? []).map((o) => (\n <label className=\"ckw-opt\" key={o}>\n <input\n type={multi ? \"checkbox\" : \"radio\"}\n name={q.key}\n checked={multi ? picked.includes(o) : value === o}\n onChange={() => onChange(multi ? (picked.includes(o) ? picked.filter((x) => x !== o) : [...picked, o]) : o)}\n />\n <span>{o}</span>\n </label>\n ))}\n </div>\n );\n }\n return (\n <textarea\n className=\"ckw-textarea\"\n aria-label={q.title}\n placeholder={q.placeholder ?? \"Type your answer\"}\n value={(value as string | undefined) ?? \"\"}\n maxLength={2000}\n onChange={(e) => onChange(e.target.value)}\n onKeyDown={(e) => (e.metaKey || e.ctrlKey) && e.key === \"Enter\" && onEnter()}\n />\n );\n};\n\nconst Card = ({ survey, onClose }: { survey: SurveyDefinition; onClose: () => void }) => {\n const api = useApi();\n const persist = useCtx().effectiveConsent === \"cookies\";\n const [step, setStep] = useState(0);\n const [answers, setAnswers] = useState<Record<string, SurveyAnswer>>({});\n const [done, setDone] = useState(false);\n const [error, setError] = useState(\"\");\n const started = useRef(Date.now());\n const finished = useRef(false);\n const live = useRef(answers);\n live.current = answers;\n const heading = useRef<HTMLHeadingElement>(null);\n const q = survey.questions[step];\n const last = step === survey.questions.length - 1;\n const has = isAnswered(answers[q?.key]);\n\n useEffect(() => {\n api.seen(survey.id).catch(() => undefined);\n }, [api, survey.id]);\n useEffect(() => {\n heading.current?.focus();\n }, [step]);\n\n const finish = useCallback(\n async (completed: boolean) => {\n if (finished.current) return;\n finished.current = true;\n remember(survey.id, persist);\n api.respond(survey.id, { answers: live.current, completed, seconds: Math.round((Date.now() - started.current) / 1000) }).catch(() => undefined);\n },\n [api, survey.id, persist],\n );\n\n // After the thank-you screen the card closes by itself.\n useEffect(() => {\n if (!done) return;\n const t = window.setTimeout(onClose, 2500);\n return () => window.clearTimeout(t);\n }, [done, onClose]);\n\n const dismiss = useCallback(() => {\n // Partial answers still count, so nothing the visitor typed is lost.\n if (Object.values(live.current).some(isAnswered)) void finish(false);\n else remember(survey.id, persist);\n onClose();\n }, [finish, onClose, survey.id, persist]);\n\n useEffect(() => {\n const h = (e: KeyboardEvent) => e.key === \"Escape\" && !done && dismiss();\n document.addEventListener(\"keydown\", h);\n return () => document.removeEventListener(\"keydown\", h);\n }, [dismiss, done]);\n\n const next = async () => {\n if (q.required && !has) {\n setError(\"Please answer this question.\");\n return;\n }\n setError(\"\");\n if (!last) return setStep(step + 1);\n await finish(true);\n setDone(true);\n };\n\n if (done)\n return (\n <div className=\"ckw-thanks\" role=\"status\">\n <span className=\"ckw-tick\">\n <Check />\n </span>\n <b>{survey.thanks || \"Thanks for your feedback.\"}</b>\n </div>\n );\n\n return (\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void next();\n }}\n >\n <div className=\"ckw-sv-top\">\n <div className=\"ckw-dots\" role=\"img\" aria-label={`Question ${step + 1} of ${survey.questions.length}`}>\n {survey.questions.map((x, i) => (\n <span className=\"ckw-dot\" key={x.key} data-on={i <= step} />\n ))}\n </div>\n <button type=\"button\" className=\"ckw-btn ckw-btn-ghost\" onClick={dismiss} aria-label=\"Close survey\" style={{ height: 24, padding: \"0 4px\" }}>\n <Cross />\n </button>\n </div>\n <h3 className=\"ckw-q\" tabIndex={-1} ref={heading}>\n {q.title}\n {!q.required && <span className=\"ckw-time\"> optional</span>}\n </h3>\n {q.description && <p className=\"ckw-qd\">{q.description}</p>}\n <Question q={q} value={answers[q.key]} onChange={(v) => (setAnswers((a) => ({ ...a, [q.key]: v })), setError(\"\"))} onEnter={() => void next()} />\n <div aria-live=\"polite\">\n {error && (\n <p className=\"ckw-qd\" style={{ color: \"var(--ckw-danger)\", margin: \"8px 0 0\" }}>\n {error}\n </p>\n )}\n </div>\n <div className=\"ckw-sv-foot\">\n <span />\n <button type=\"submit\" className=\"ckw-btn ckw-btn-primary\">\n {last ? \"Send\" : \"Next\"}\n </button>\n </div>\n </form>\n );\n};\n\n/** Asks the visitor the survey Cockpit picked for this page. Mounted by CockpitProvider unless `surveys={false}`. */\nexport const Survey = () => {\n const { embedded, distinctId, effectiveConsent } = useCtx();\n const api = useApi();\n const [survey, setSurvey] = useState<SurveyDefinition | null>(null);\n // Never inside Cockpit previews, never without consent, and not before the visitor id is known.\n const off = embedded || effectiveConsent === \"off\" || distinctId === \"anonymous\";\n const persist = effectiveConsent === \"cookies\";\n\n useEffect(() => {\n if (off) return;\n const ctl = new AbortController();\n const timer = window.setTimeout(() => {\n api.survey(window.location.href, ctl.signal)\n .then((r) => {\n const s = r.survey;\n if (!s || s.questions.length === 0 || wasDismissed(s.id, persist)) return;\n setSurvey(s);\n })\n .catch(() => undefined);\n }, 3000);\n return () => {\n window.clearTimeout(timer);\n ctl.abort();\n };\n }, [api, off, persist]);\n\n const close = useCallback(() => setSurvey(null), []);\n if (!survey || off) return null;\n return (\n <Root>\n <div className=\"ckw-survey\" role=\"dialog\" aria-label={survey.name}>\n <Card key={survey.id} survey={survey} onClose={close} />\n </div>\n </Root>\n );\n};\n"],"mappings":"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","names":[]}
package/dist/ui.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { type ReactNode } from "react";
2
+ import { type FeedbackPost, type PostStatus } from "./api.js";
3
+ export type Theme = "light" | "dark" | "auto";
4
+ /** The scoped wrapper every widget renders inside. Carries the accent and the theme. */
5
+ export declare const Root: ({ theme, accent, className, children }: {
6
+ theme?: Theme;
7
+ accent?: string;
8
+ className?: string;
9
+ children: ReactNode;
10
+ }) => import("react").JSX.Element;
11
+ export declare const ChevronUp: () => import("react").JSX.Element;
12
+ export declare const Plus: () => import("react").JSX.Element;
13
+ export declare const Cross: () => import("react").JSX.Element;
14
+ export declare const Check: () => import("react").JSX.Element;
15
+ export declare const Chat: () => import("react").JSX.Element;
16
+ export declare const Star: () => import("react").JSX.Element;
17
+ export declare const StatusPill: ({ status }: {
18
+ status: PostStatus;
19
+ }) => import("react").JSX.Element;
20
+ export declare const VoteButton: ({ post, onVote, small }: {
21
+ post: FeedbackPost;
22
+ onVote: (p: FeedbackPost) => void;
23
+ small?: boolean;
24
+ }) => import("react").JSX.Element;
25
+ export declare const timeAgo: (iso: string) => string;
26
+ /** Calls `onClose` on Esc while mounted. */
27
+ export declare const useEscape: (onClose: () => void) => void;
28
+ /** Up to three existing posts that share words with what the visitor is typing. */
29
+ export declare const similarPosts: (title: string, posts: FeedbackPost[]) => FeedbackPost[];
package/dist/ui.js ADDED
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ // src/ui.tsx
4
+ import { useEffect, useRef } from "react";
5
+ import { STATUS_LABEL } from "./api.js";
6
+ import { useStyles } from "./styles.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ var Root = ({ theme = "auto", accent = "#3f76ff", className, children }) => {
9
+ useStyles();
10
+ return /* @__PURE__ */ jsx("div", { className: `ckw-root${className ? ` ${className}` : ""}`, "data-theme": theme, style: { ["--ckw-accent"]: accent }, children });
11
+ };
12
+ var Svg = ({ children, size = 16 }) => /* @__PURE__ */ jsx("svg", { className: "ckw-icon", width: size, height: size, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children });
13
+ var ChevronUp = () => /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M3.5 10 8 5.5 12.5 10" }) });
14
+ var Plus = () => /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M8 3v10M3 8h10" }) });
15
+ var Cross = () => /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "m4 4 8 8M12 4l-8 8" }) });
16
+ var Check = () => /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "m3 8.5 3.2 3.2L13 4.8" }) });
17
+ var Chat = () => /* @__PURE__ */ jsx(Svg, { children: /* @__PURE__ */ jsx("path", { d: "M2.5 4.5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2H7l-3 2.5V11a2 2 0 0 1-1.5-2z" }) });
18
+ var Star = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "m12 2.8 2.8 5.8 6.4.9-4.6 4.5 1.1 6.3L12 17.2l-5.7 3.1 1.1-6.3L2.8 9.5l6.4-.9z" }) });
19
+ var StatusPill = ({ status }) => /* @__PURE__ */ jsx("span", { className: "ckw-pill", "data-s": status, children: STATUS_LABEL[status] ?? status });
20
+ var VoteButton = ({ post, onVote, small }) => /* @__PURE__ */ jsxs(
21
+ "button",
22
+ {
23
+ type: "button",
24
+ className: `ckw-vote${small ? " ckw-vote-sm" : ""}`,
25
+ "aria-pressed": post.voted,
26
+ "aria-label": `${post.voted ? "Remove your vote from" : "Upvote"} ${post.title}. ${post.votes} votes`,
27
+ onClick: () => onVote(post),
28
+ children: [
29
+ /* @__PURE__ */ jsx(ChevronUp, {}),
30
+ /* @__PURE__ */ jsx("span", { children: post.votes })
31
+ ]
32
+ }
33
+ );
34
+ var timeAgo = (iso) => {
35
+ const t = new Date(iso).getTime();
36
+ if (Number.isNaN(t)) return "";
37
+ const d = Math.max(0, Date.now() - t) / 1e3;
38
+ if (d < 60) return "just now";
39
+ if (d < 3600) return `${Math.floor(d / 60)}m ago`;
40
+ if (d < 86400) return `${Math.floor(d / 3600)}h ago`;
41
+ if (d < 86400 * 30) return `${Math.floor(d / 86400)}d ago`;
42
+ return new Date(iso).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" });
43
+ };
44
+ var useEscape = (onClose) => {
45
+ const ref = useRef(onClose);
46
+ ref.current = onClose;
47
+ useEffect(() => {
48
+ const h = (e) => e.key === "Escape" && ref.current();
49
+ document.addEventListener("keydown", h);
50
+ return () => document.removeEventListener("keydown", h);
51
+ }, []);
52
+ };
53
+ var words = (s) => s.toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 2 && !["the", "and", "for", "with", "add", "can", "please", "want", "would", "our", "you", "have"].includes(w));
54
+ var similarPosts = (title, posts) => {
55
+ const mine = words(title);
56
+ if (mine.length === 0 || title.trim().length < 4) return [];
57
+ return posts.map((p) => {
58
+ const theirs = new Set(words(`${p.title} ${p.body}`.slice(0, 300)));
59
+ const hit = mine.filter((w) => theirs.has(w) || [...theirs].some((t) => t.startsWith(w) || w.startsWith(t))).length;
60
+ return { p, score: hit / mine.length };
61
+ }).filter((x) => x.score >= 0.5).sort((a, b) => b.score - a.score || b.p.votes - a.p.votes).slice(0, 3).map((x) => x.p);
62
+ };
63
+ export {
64
+ Chat,
65
+ Check,
66
+ ChevronUp,
67
+ Cross,
68
+ Plus,
69
+ Root,
70
+ Star,
71
+ StatusPill,
72
+ VoteButton,
73
+ similarPosts,
74
+ timeAgo,
75
+ useEscape
76
+ };
77
+ //# sourceMappingURL=ui.js.map
package/dist/ui.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ui.tsx"],"sourcesContent":["\"use client\";\n\nimport { type ReactNode, useEffect, useRef } from \"react\";\nimport { type FeedbackPost, type PostStatus, STATUS_LABEL } from \"./api\";\nimport { useStyles } from \"./styles\";\n\nexport type Theme = \"light\" | \"dark\" | \"auto\";\n\n/** The scoped wrapper every widget renders inside. Carries the accent and the theme. */\nexport const Root = ({ theme = \"auto\", accent = \"#3f76ff\", className, children }: { theme?: Theme; accent?: string; className?: string; children: ReactNode }) => {\n useStyles();\n return (\n <div className={`ckw-root${className ? ` ${className}` : \"\"}`} data-theme={theme} style={{ [\"--ckw-accent\" as string]: accent }}>\n {children}\n </div>\n );\n};\n\nconst Svg = ({ children, size = 16 }: { children: ReactNode; size?: number }) => (\n <svg className=\"ckw-icon\" width={size} height={size} viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"1.8\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n {children}\n </svg>\n);\n\nexport const ChevronUp = () => (\n <Svg>\n <path d=\"M3.5 10 8 5.5 12.5 10\" />\n </Svg>\n);\nexport const Plus = () => (\n <Svg>\n <path d=\"M8 3v10M3 8h10\" />\n </Svg>\n);\nexport const Cross = () => (\n <Svg>\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </Svg>\n);\nexport const Check = () => (\n <Svg>\n <path d=\"m3 8.5 3.2 3.2L13 4.8\" />\n </Svg>\n);\nexport const Chat = () => (\n <Svg>\n <path d=\"M2.5 4.5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v4.5a2 2 0 0 1-2 2H7l-3 2.5V11a2 2 0 0 1-1.5-2z\" />\n </Svg>\n);\nexport const Star = () => (\n <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" aria-hidden=\"true\">\n <path d=\"m12 2.8 2.8 5.8 6.4.9-4.6 4.5 1.1 6.3L12 17.2l-5.7 3.1 1.1-6.3L2.8 9.5l6.4-.9z\" />\n </svg>\n);\n\nexport const StatusPill = ({ status }: { status: PostStatus }) => (\n <span className=\"ckw-pill\" data-s={status}>\n {STATUS_LABEL[status] ?? status}\n </span>\n);\n\nexport const VoteButton = ({ post, onVote, small }: { post: FeedbackPost; onVote: (p: FeedbackPost) => void; small?: boolean }) => (\n <button\n type=\"button\"\n className={`ckw-vote${small ? \" ckw-vote-sm\" : \"\"}`}\n aria-pressed={post.voted}\n aria-label={`${post.voted ? \"Remove your vote from\" : \"Upvote\"} ${post.title}. ${post.votes} votes`}\n onClick={() => onVote(post)}\n >\n <ChevronUp />\n <span>{post.votes}</span>\n </button>\n);\n\nexport const timeAgo = (iso: string) => {\n const t = new Date(iso).getTime();\n if (Number.isNaN(t)) return \"\";\n const d = Math.max(0, Date.now() - t) / 1000;\n if (d < 60) return \"just now\";\n if (d < 3600) return `${Math.floor(d / 60)}m ago`;\n if (d < 86400) return `${Math.floor(d / 3600)}h ago`;\n if (d < 86400 * 30) return `${Math.floor(d / 86400)}d ago`;\n return new Date(iso).toLocaleDateString(undefined, { month: \"short\", day: \"numeric\", year: \"numeric\" });\n};\n\n/** Calls `onClose` on Esc while mounted. */\nexport const useEscape = (onClose: () => void) => {\n const ref = useRef(onClose);\n ref.current = onClose;\n useEffect(() => {\n const h = (e: KeyboardEvent) => e.key === \"Escape\" && ref.current();\n document.addEventListener(\"keydown\", h);\n return () => document.removeEventListener(\"keydown\", h);\n }, []);\n};\n\n/** Words of a title, lowercased, without filler. */\nconst words = (s: string) =>\n s\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((w) => w.length > 2 && ![\"the\", \"and\", \"for\", \"with\", \"add\", \"can\", \"please\", \"want\", \"would\", \"our\", \"you\", \"have\"].includes(w));\n\n/** Up to three existing posts that share words with what the visitor is typing. */\nexport const similarPosts = (title: string, posts: FeedbackPost[]) => {\n const mine = words(title);\n if (mine.length === 0 || title.trim().length < 4) return [];\n return posts\n .map((p) => {\n const theirs = new Set(words(`${p.title} ${p.body}`.slice(0, 300)));\n const hit = mine.filter((w) => theirs.has(w) || [...theirs].some((t) => t.startsWith(w) || w.startsWith(t))).length;\n return { p, score: hit / mine.length };\n })\n .filter((x) => x.score >= 0.5)\n .sort((a, b) => b.score - a.score || b.p.votes - a.p.votes)\n .slice(0, 3)\n .map((x) => x.p);\n};\n"],"mappings":"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","names":[]}