@vespera-ui/vue 0.3.0 → 0.5.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/README.md CHANGED
@@ -39,7 +39,10 @@ Form components support `v-model` (`Input`, `Textarea`, `Switch`, `Checkbox`). B
39
39
  ## Components
40
40
 
41
41
  `Button`, `IconButton`, `Badge`, `Tag`, `Kbd`, `Divider`, `Spinner`, `Card`, `CardHead`, `Alert`,
42
- `Field`, `Input`, `Textarea`, `Switch`, `Checkbox`. More are being ported — the CSS layer already
43
- supports every Vespera component, so wrappers are thin.
42
+ `Field`, `Input`, `Textarea`, `NativeSelect`, `Switch`, `Checkbox`, `Radio`, `RadioGroup`,
43
+ `Slider`, `Progress`, `Skeleton`, `Avatar`, `AvatarGroup`, `Segmented`, `Tabs`, `Breadcrumb`,
44
+ `Pagination`, `Stepper`, `CircularProgress`, `Stat`, `Timeline`, `DescriptionList`, `Banner`,
45
+ `EmptyState`, `Accordion`. The overlay/portal components (dialogs, menus, toasts, the themed
46
+ select, date pickers) are React-only for now — use the raw classes for those in other frameworks.
44
47
 
45
48
  License: Apache-2.0
package/dist/index.cjs CHANGED
@@ -32,14 +32,18 @@ __export(index_exports, {
32
32
  CardHead: () => CardHead,
33
33
  Checkbox: () => Checkbox,
34
34
  CircularProgress: () => CircularProgress,
35
+ CopyButton: () => CopyButton,
35
36
  DescriptionList: () => DescriptionList,
36
37
  Divider: () => Divider,
38
+ Donut: () => Donut,
37
39
  EmptyState: () => EmptyState,
38
40
  Field: () => Field,
39
41
  IconButton: () => IconButton,
42
+ InlineEdit: () => InlineEdit,
40
43
  Input: () => Input,
41
44
  Kbd: () => Kbd,
42
45
  NativeSelect: () => NativeSelect,
46
+ NumberStepper: () => NumberStepper,
43
47
  Pagination: () => Pagination,
44
48
  Progress: () => Progress,
45
49
  Radio: () => Radio,
@@ -47,14 +51,17 @@ __export(index_exports, {
47
51
  Segmented: () => Segmented,
48
52
  Skeleton: () => Skeleton,
49
53
  Slider: () => Slider,
54
+ Sparkline: () => Sparkline,
50
55
  Spinner: () => Spinner,
51
56
  Stat: () => Stat,
57
+ StatCard: () => StatCard,
52
58
  Stepper: () => Stepper,
53
59
  Switch: () => Switch,
54
60
  Tabs: () => Tabs,
55
61
  Tag: () => Tag,
56
62
  Textarea: () => Textarea,
57
- Timeline: () => Timeline
63
+ Timeline: () => Timeline,
64
+ smoothPath: () => smoothPath
58
65
  });
59
66
  module.exports = __toCommonJS(index_exports);
60
67
  var import_vue = require("vue");
@@ -1032,6 +1039,371 @@ var Accordion = (0, import_vue.defineComponent)({
1032
1039
  );
1033
1040
  }
1034
1041
  });
1042
+ function smoothPath(pts) {
1043
+ if (pts.length < 2) return "";
1044
+ let d = `M ${pts[0][0]} ${pts[0][1]}`;
1045
+ for (let i = 0; i < pts.length - 1; i++) {
1046
+ const [x0, y0] = pts[i];
1047
+ const [x1, y1] = pts[i + 1];
1048
+ const cx2 = (x0 + x1) / 2;
1049
+ d += ` C ${cx2} ${y0} ${cx2} ${y1} ${x1} ${y1}`;
1050
+ }
1051
+ return d;
1052
+ }
1053
+ var Sparkline = (0, import_vue.defineComponent)({
1054
+ name: "VspSparkline",
1055
+ props: {
1056
+ data: { type: Array, default: () => [] },
1057
+ color: { type: String, default: "var(--accent)" },
1058
+ w: { type: Number, default: 110 },
1059
+ h: { type: Number, default: 34 },
1060
+ fill: { type: Boolean, default: true }
1061
+ },
1062
+ setup(props) {
1063
+ const gid = "spk" + (0, import_vue.useId)().replace(/[^a-zA-Z0-9]/g, "");
1064
+ return () => {
1065
+ const data = props.data;
1066
+ const min = Math.min(...data);
1067
+ const max = Math.max(...data);
1068
+ const rng = max - min || 1;
1069
+ const pts = data.map((v, i) => [
1070
+ i / (data.length - 1) * props.w,
1071
+ props.h - 3 - (v - min) / rng * (props.h - 6)
1072
+ ]);
1073
+ const d = smoothPath(pts);
1074
+ const last = pts[pts.length - 1] ?? [0, 0];
1075
+ return (0, import_vue.h)(
1076
+ "svg",
1077
+ {
1078
+ width: props.w,
1079
+ height: props.h,
1080
+ viewBox: `0 0 ${props.w} ${props.h}`,
1081
+ style: { display: "block", overflow: "visible" }
1082
+ },
1083
+ [
1084
+ props.fill ? (0, import_vue.h)("defs", null, [
1085
+ (0, import_vue.h)("linearGradient", { id: gid, x1: "0", x2: "0", y1: "0", y2: "1" }, [
1086
+ (0, import_vue.h)("stop", { offset: "0", "stop-color": props.color, "stop-opacity": "0.28" }),
1087
+ (0, import_vue.h)("stop", { offset: "1", "stop-color": props.color, "stop-opacity": "0" })
1088
+ ])
1089
+ ]) : null,
1090
+ props.fill ? (0, import_vue.h)("path", {
1091
+ d: `${d} L ${props.w} ${props.h} L 0 ${props.h} Z`,
1092
+ fill: `url(#${gid})`
1093
+ }) : null,
1094
+ (0, import_vue.h)("path", {
1095
+ d,
1096
+ fill: "none",
1097
+ stroke: props.color,
1098
+ "stroke-width": "2",
1099
+ "stroke-linecap": "round"
1100
+ }),
1101
+ (0, import_vue.h)("circle", { cx: last[0], cy: last[1], r: "2.6", fill: props.color })
1102
+ ]
1103
+ );
1104
+ };
1105
+ }
1106
+ });
1107
+ var Donut = (0, import_vue.defineComponent)({
1108
+ name: "VspDonut",
1109
+ props: {
1110
+ data: { type: Array, default: () => [] },
1111
+ size: { type: Number, default: 168 },
1112
+ thickness: { type: Number, default: 22 }
1113
+ },
1114
+ setup(props) {
1115
+ return () => {
1116
+ const total = props.data.reduce((s, d) => s + d.value, 0) || 1;
1117
+ const r = (props.size - props.thickness) / 2;
1118
+ const c = props.size / 2;
1119
+ const circ = 2 * Math.PI * r;
1120
+ let acc = 0;
1121
+ const segs = props.data.map((d, i) => {
1122
+ const len = d.value / total * circ;
1123
+ const seg = (0, import_vue.h)("circle", {
1124
+ key: i,
1125
+ cx: c,
1126
+ cy: c,
1127
+ r,
1128
+ fill: "none",
1129
+ stroke: d.color,
1130
+ "stroke-width": props.thickness,
1131
+ "stroke-dasharray": `${len - 2.5} ${circ - len + 2.5}`,
1132
+ "stroke-dashoffset": -acc,
1133
+ "stroke-linecap": "round"
1134
+ });
1135
+ acc += len;
1136
+ return seg;
1137
+ });
1138
+ return (0, import_vue.h)("div", { style: { display: "flex", alignItems: "center", gap: "22px" } }, [
1139
+ (0, import_vue.h)(
1140
+ "svg",
1141
+ {
1142
+ width: props.size,
1143
+ height: props.size,
1144
+ style: { transform: "rotate(-90deg)", flexShrink: 0 }
1145
+ },
1146
+ [
1147
+ (0, import_vue.h)("circle", {
1148
+ cx: c,
1149
+ cy: c,
1150
+ r,
1151
+ fill: "none",
1152
+ stroke: "var(--surface-3)",
1153
+ "stroke-width": props.thickness
1154
+ }),
1155
+ ...segs
1156
+ ]
1157
+ ),
1158
+ (0, import_vue.h)(
1159
+ "div",
1160
+ { style: { display: "flex", flexDirection: "column", gap: "9px", flex: 1 } },
1161
+ props.data.map(
1162
+ (d, i) => (0, import_vue.h)(
1163
+ "div",
1164
+ {
1165
+ key: i,
1166
+ style: { display: "flex", alignItems: "center", gap: "9px", fontSize: "12.5px" }
1167
+ },
1168
+ [
1169
+ (0, import_vue.h)("i", {
1170
+ style: {
1171
+ width: "9px",
1172
+ height: "9px",
1173
+ borderRadius: "3px",
1174
+ background: d.color,
1175
+ flexShrink: 0
1176
+ }
1177
+ }),
1178
+ (0, import_vue.h)("span", { style: { color: "var(--text-dim)", flex: 1 } }, d.label),
1179
+ (0, import_vue.h)(
1180
+ "span",
1181
+ { class: "mono tnum", style: { fontWeight: 600 } },
1182
+ `${Math.round(d.value / total * 100)}%`
1183
+ )
1184
+ ]
1185
+ )
1186
+ )
1187
+ )
1188
+ ]);
1189
+ };
1190
+ }
1191
+ });
1192
+ var StatCard = (0, import_vue.defineComponent)({
1193
+ name: "VspStatCard",
1194
+ props: {
1195
+ label: { type: String, default: void 0 },
1196
+ value: { type: String, default: void 0 },
1197
+ delta: { type: String, default: void 0 },
1198
+ deltaDir: { type: String, default: "up" },
1199
+ spark: { type: Array, default: void 0 },
1200
+ sparkColor: { type: String, default: "var(--accent)" }
1201
+ },
1202
+ setup(props, { slots }) {
1203
+ return () => (0, import_vue.h)(
1204
+ "div",
1205
+ {
1206
+ class: "card card-pad vsp-rise",
1207
+ style: { display: "flex", flexDirection: "column", gap: "14px" }
1208
+ },
1209
+ [
1210
+ (0, import_vue.h)(
1211
+ "div",
1212
+ { style: { display: "flex", alignItems: "center", justifyContent: "space-between" } },
1213
+ [
1214
+ (0, import_vue.h)("div", { style: { display: "flex", alignItems: "center", gap: "10px" } }, [
1215
+ (0, import_vue.h)(
1216
+ "span",
1217
+ {
1218
+ style: {
1219
+ width: "34px",
1220
+ height: "34px",
1221
+ borderRadius: "var(--r-sm)",
1222
+ display: "grid",
1223
+ placeItems: "center",
1224
+ background: "color-mix(in oklab, var(--accent) 13%, transparent)",
1225
+ color: "var(--accent)"
1226
+ }
1227
+ },
1228
+ slots.icon?.()
1229
+ ),
1230
+ (0, import_vue.h)("span", { class: "eyebrow" }, props.label)
1231
+ ]),
1232
+ props.delta != null ? (0, import_vue.h)(
1233
+ "span",
1234
+ { class: cx("badge", props.deltaDir === "up" ? "badge-pos" : "badge-neg") },
1235
+ [
1236
+ svgIcon(
1237
+ props.deltaDir === "up" ? "M12 19V5M5 12l7-7 7 7" : "M12 5v14M5 12l7 7 7-7",
1238
+ 11
1239
+ ),
1240
+ props.delta
1241
+ ]
1242
+ ) : null
1243
+ ]
1244
+ ),
1245
+ (0, import_vue.h)(
1246
+ "div",
1247
+ {
1248
+ style: {
1249
+ display: "flex",
1250
+ alignItems: "flex-end",
1251
+ justifyContent: "space-between",
1252
+ gap: "12px"
1253
+ }
1254
+ },
1255
+ [
1256
+ (0, import_vue.h)(
1257
+ "div",
1258
+ {
1259
+ class: "tnum",
1260
+ style: {
1261
+ fontSize: "30px",
1262
+ fontWeight: 800,
1263
+ letterSpacing: "-.02em",
1264
+ lineHeight: 1
1265
+ }
1266
+ },
1267
+ props.value
1268
+ ),
1269
+ props.spark ? (0, import_vue.h)(Sparkline, { data: props.spark, color: props.sparkColor }) : null
1270
+ ]
1271
+ )
1272
+ ]
1273
+ );
1274
+ }
1275
+ });
1276
+ var ICON_CHECK = "M20 6L9 17l-5-5";
1277
+ var ICON_DOC = "M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8zM14 2v6h6";
1278
+ var ICON_PENCIL = "M12 20h9M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4z";
1279
+ var NumberStepper = (0, import_vue.defineComponent)({
1280
+ name: "VspNumberStepper",
1281
+ props: {
1282
+ modelValue: { type: Number, default: 0 },
1283
+ min: { type: Number, default: void 0 },
1284
+ max: { type: Number, default: void 0 },
1285
+ step: { type: Number, default: 1 },
1286
+ unit: { type: String, default: void 0 }
1287
+ },
1288
+ emits: ["update:modelValue"],
1289
+ setup(props, { emit }) {
1290
+ const set = (v) => {
1291
+ let n = v;
1292
+ if (props.min != null && n < props.min) n = props.min;
1293
+ if (props.max != null && n > props.max) n = props.max;
1294
+ emit("update:modelValue", n);
1295
+ };
1296
+ return () => (0, import_vue.h)("div", { class: "ui-stepper" }, [
1297
+ (0, import_vue.h)(
1298
+ "button",
1299
+ {
1300
+ type: "button",
1301
+ "aria-label": "Decrease",
1302
+ disabled: props.min != null && props.modelValue <= props.min,
1303
+ onClick: () => set(props.modelValue - props.step)
1304
+ },
1305
+ "\u2212"
1306
+ ),
1307
+ (0, import_vue.h)("span", { class: "val" }, [
1308
+ props.modelValue,
1309
+ props.unit ? (0, import_vue.h)("i", null, props.unit) : null
1310
+ ]),
1311
+ (0, import_vue.h)(
1312
+ "button",
1313
+ {
1314
+ type: "button",
1315
+ "aria-label": "Increase",
1316
+ disabled: props.max != null && props.modelValue >= props.max,
1317
+ onClick: () => set(props.modelValue + props.step)
1318
+ },
1319
+ "+"
1320
+ )
1321
+ ]);
1322
+ }
1323
+ });
1324
+ var CopyButton = (0, import_vue.defineComponent)({
1325
+ name: "VspCopyButton",
1326
+ props: {
1327
+ text: { type: String, required: true },
1328
+ label: { type: String, default: "Copy" },
1329
+ size: { type: String, default: "sm" }
1330
+ },
1331
+ setup(props) {
1332
+ const done = (0, import_vue.ref)(false);
1333
+ const copy = async () => {
1334
+ try {
1335
+ await navigator.clipboard?.writeText(props.text);
1336
+ } catch {
1337
+ }
1338
+ done.value = true;
1339
+ setTimeout(() => done.value = false, 1400);
1340
+ };
1341
+ return () => (0, import_vue.h)(
1342
+ "button",
1343
+ {
1344
+ type: "button",
1345
+ class: cx("btn", "btn-ghost", props.size === "sm" && "btn-sm"),
1346
+ onClick: copy
1347
+ },
1348
+ [
1349
+ done.value ? (0, import_vue.h)("span", { style: { color: "var(--success)", display: "inline-flex" } }, [
1350
+ svgIcon(ICON_CHECK, 15)
1351
+ ]) : svgIcon(ICON_DOC, 15),
1352
+ done.value ? "Copied" : props.label
1353
+ ]
1354
+ );
1355
+ }
1356
+ });
1357
+ var InlineEdit = (0, import_vue.defineComponent)({
1358
+ name: "VspInlineEdit",
1359
+ props: {
1360
+ value: { type: String, default: "" },
1361
+ placeholder: { type: String, default: "Empty" }
1362
+ },
1363
+ emits: ["save"],
1364
+ setup(props, { emit }) {
1365
+ const editing = (0, import_vue.ref)(false);
1366
+ const draft = (0, import_vue.ref)(props.value);
1367
+ const commit = () => {
1368
+ editing.value = false;
1369
+ if (draft.value !== props.value) emit("save", draft.value);
1370
+ };
1371
+ return () => editing.value ? (0, import_vue.h)("input", {
1372
+ class: "ui-input",
1373
+ value: draft.value,
1374
+ style: { height: "32px", maxWidth: "240px" },
1375
+ onVnodeMounted: (vn) => vn.el?.focus(),
1376
+ onInput: (e) => draft.value = e.target.value,
1377
+ onBlur: commit,
1378
+ onKeydown: (e) => {
1379
+ if (e.key === "Enter") commit();
1380
+ if (e.key === "Escape") {
1381
+ draft.value = props.value;
1382
+ editing.value = false;
1383
+ }
1384
+ }
1385
+ }) : (0, import_vue.h)(
1386
+ "span",
1387
+ {
1388
+ class: "ui-inline",
1389
+ onClick: () => {
1390
+ draft.value = props.value;
1391
+ editing.value = true;
1392
+ }
1393
+ },
1394
+ [
1395
+ (0, import_vue.h)(
1396
+ "span",
1397
+ { style: { color: props.value ? "var(--text)" : "var(--text-faint)" } },
1398
+ props.value || props.placeholder
1399
+ ),
1400
+ (0, import_vue.h)("span", { class: "pen", style: { display: "inline-flex" } }, [
1401
+ svgIcon(ICON_PENCIL, 14)
1402
+ ])
1403
+ ]
1404
+ );
1405
+ }
1406
+ });
1035
1407
  // Annotate the CommonJS export names for ESM import in node:
1036
1408
  0 && (module.exports = {
1037
1409
  Accordion,
@@ -1046,14 +1418,18 @@ var Accordion = (0, import_vue.defineComponent)({
1046
1418
  CardHead,
1047
1419
  Checkbox,
1048
1420
  CircularProgress,
1421
+ CopyButton,
1049
1422
  DescriptionList,
1050
1423
  Divider,
1424
+ Donut,
1051
1425
  EmptyState,
1052
1426
  Field,
1053
1427
  IconButton,
1428
+ InlineEdit,
1054
1429
  Input,
1055
1430
  Kbd,
1056
1431
  NativeSelect,
1432
+ NumberStepper,
1057
1433
  Pagination,
1058
1434
  Progress,
1059
1435
  Radio,
@@ -1061,13 +1437,16 @@ var Accordion = (0, import_vue.defineComponent)({
1061
1437
  Segmented,
1062
1438
  Skeleton,
1063
1439
  Slider,
1440
+ Sparkline,
1064
1441
  Spinner,
1065
1442
  Stat,
1443
+ StatCard,
1066
1444
  Stepper,
1067
1445
  Switch,
1068
1446
  Tabs,
1069
1447
  Tag,
1070
1448
  Textarea,
1071
- Timeline
1449
+ Timeline,
1450
+ smoothPath
1072
1451
  });
1073
1452
  //# sourceMappingURL=index.cjs.map