@designbasekorea/figma-ui 0.6.10 → 0.7.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/index.esm.js CHANGED
@@ -38,6 +38,7 @@ const FIGMA_UI_TRANSLATIONS = {
38
38
  remainingShort: '남음',
39
39
  usageCountCaption: '오늘 사용',
40
40
  remainingCopy: '{{count}}회 남음 · 매일 리셋',
41
+ lowUsageWarning: '무료 사용량이 {{count}}회({{percent}}%) 남았습니다. 멤버십으로 무제한 이용해보세요.',
41
42
  usageUnavailable: '사용량 제한이 없습니다',
42
43
  unlimitedUsageTooltip: '모든 기능을 제한 없이 사용할 수 있습니다',
43
44
  betaUsageDescription: '베타 기간에는 사용량 제한이 적용되지 않습니다',
@@ -170,6 +171,7 @@ const FIGMA_UI_TRANSLATIONS = {
170
171
  remainingShort: 'left',
171
172
  usageCountCaption: 'Used today',
172
173
  remainingCopy: '{{count}} remaining · resets daily',
174
+ lowUsageWarning: 'Only {{count}} free uses ({{percent}}%) remain. Upgrade for unlimited access.',
173
175
  usageUnavailable: 'No usage limit',
174
176
  unlimitedUsageTooltip: 'Use every feature without limits',
175
177
  betaUsageDescription: 'Usage limits are not applied during the beta period',
@@ -302,6 +304,7 @@ const FIGMA_UI_TRANSLATIONS = {
302
304
  remainingShort: '残り',
303
305
  usageCountCaption: '今日の使用',
304
306
  remainingCopy: '{{count}}回残り · 毎日リセット',
307
+ lowUsageWarning: '無料使用回数は残り{{count}}回({{percent}}%)です。無制限利用にアップグレードできます。',
305
308
  usageUnavailable: '使用制限はありません',
306
309
  unlimitedUsageTooltip: 'すべての機能を無制限で使用できます',
307
310
  betaUsageDescription: 'ベータ期間中は使用制限がありません',
@@ -979,7 +982,7 @@ const fallbackText = (t, key, fallback) => {
979
982
  const translated = t?.(key);
980
983
  return translated && translated !== key ? translated : fallback;
981
984
  };
982
- const UsageStatusPopover = ({ paymentStatus, usageCount = 0, maxDailyUsage = 20, showUsage = true, showPlan = true, isLoading = false, onLicensePageClick, t, locale, className, }) => {
985
+ const UsageStatusPopover = ({ paymentStatus, usageCount = 0, remainingUsage, maxDailyUsage = 20, showUsage = true, showPlan = true, isLoading = false, onLicensePageClick, t, locale, className, }) => {
983
986
  const { t: translate } = useFigmaUiI18n(locale, t);
984
987
  const [isOpen, setIsOpen] = useState(false);
985
988
  const rootRef = useRef(null);
@@ -989,9 +992,11 @@ const UsageStatusPopover = ({ paymentStatus, usageCount = 0, maxDailyUsage = 20,
989
992
  const isBeta = paymentStatus === 'BETA';
990
993
  const hasDailyLimit = !isPaid && maxDailyUsage > 0;
991
994
  const limit = Math.max(1, Math.round(maxDailyUsage));
992
- const used = Math.min(limit, Math.max(0, Math.round(usageCount)));
993
- const remaining = Math.max(0, limit - used);
994
- const usedPercent = hasDailyLimit ? Math.round((used / limit) * 100) : 0;
995
+ const hasRemainingUsage = typeof remainingUsage === 'number' && Number.isFinite(remainingUsage);
996
+ const remaining = hasRemainingUsage
997
+ ? Math.min(limit, Math.max(0, Math.round(remainingUsage)))
998
+ : Math.max(0, limit - Math.min(limit, Math.max(0, Math.round(usageCount))));
999
+ const remainingPercent = hasDailyLimit ? Math.round((remaining / limit) * 100) : 0;
995
1000
  const hasControl = showUsage || showPlan;
996
1001
  const planLabel = isPaid
997
1002
  ? fallbackText(translate, 'proUser', '프로')
@@ -1049,7 +1054,7 @@ const UsageStatusPopover = ({ paymentStatus, usageCount = 0, maxDailyUsage = 20,
1049
1054
  React.createElement("div", { className: "designbase-figma-usage-status__control" },
1050
1055
  showUsage && (React.createElement("button", { ref: triggerRef, type: "button", className: "designbase-figma-usage-status__trigger", "aria-expanded": isOpen, "aria-controls": popoverId, "aria-haspopup": "dialog", "aria-label": isLoading ? fallbackText(translate, 'loading', '불러오는 중') : triggerLabel, disabled: isLoading, onClick: togglePopover }, isLoading ? (React.createElement("span", { className: "designbase-figma-usage-status__skeleton", "aria-hidden": "true" })) : isPaid ? (React.createElement("span", { className: "designbase-figma-usage-status__unlimited-mark", "aria-hidden": "true" }, "\u221E")) : hasDailyLimit ? (React.createElement(React.Fragment, null,
1051
1056
  React.createElement("span", { className: "designbase-figma-usage-status__mini-progress", "aria-hidden": "true" },
1052
- React.createElement("span", { style: { width: `${usedPercent}%` } })),
1057
+ React.createElement("span", { style: { width: `${remainingPercent}%` } })),
1053
1058
  React.createElement("span", { className: "designbase-figma-usage-status__usage-value" },
1054
1059
  React.createElement("strong", null, remaining),
1055
1060
  React.createElement("span", null,
@@ -1065,36 +1070,19 @@ const UsageStatusPopover = ({ paymentStatus, usageCount = 0, maxDailyUsage = 20,
1065
1070
  React.createElement("span", null, fallbackText(translate, 'unlimitedUsageTooltip', '모든 기능을 제한 없이 사용할 수 있습니다')))) : isBeta ? (React.createElement("p", { className: "designbase-figma-usage-status__note" }, fallbackText(translate, 'betaUsageDescription', '베타 기간에는 사용량 제한이 적용되지 않습니다'))) : hasDailyLimit ? (React.createElement("div", { className: "designbase-figma-usage-status__usage-detail" },
1066
1071
  React.createElement("div", { className: "designbase-figma-usage-status__usage-summary" },
1067
1072
  React.createElement("strong", { className: "designbase-figma-usage-status__usage-count" },
1068
- used,
1073
+ remaining,
1069
1074
  React.createElement("span", { className: "designbase-figma-usage-status__usage-divider" }, "/"),
1070
1075
  limit),
1071
- React.createElement("span", { className: "designbase-figma-usage-status__usage-caption" }, fallbackText(translate, 'usageCountCaption', '오늘 사용'))),
1072
- React.createElement("div", { className: "designbase-figma-usage-status__progress", role: "progressbar", "aria-label": fallbackText(translate, 'usageCountCaption', '오늘 사용'), "aria-valuemin": 0, "aria-valuemax": limit, "aria-valuenow": used, "aria-valuetext": `${used} / ${limit}` },
1073
- React.createElement("span", { style: { width: `${usedPercent}%` } })),
1076
+ React.createElement("span", { className: "designbase-figma-usage-status__usage-caption" }, fallbackText(translate, 'remainingToday', '오늘 남은 사용량')),
1077
+ React.createElement("strong", { className: "designbase-figma-usage-status__usage-percent" },
1078
+ remainingPercent,
1079
+ "%")),
1080
+ React.createElement("div", { className: "designbase-figma-usage-status__progress", role: "progressbar", "aria-label": fallbackText(translate, 'remainingToday', '오늘 남은 사용량'), "aria-valuemin": 0, "aria-valuemax": limit, "aria-valuenow": remaining, "aria-valuetext": `${remaining} / ${limit}, ${remainingPercent}%` },
1081
+ React.createElement("span", { style: { width: `${remainingPercent}%` } })),
1074
1082
  React.createElement("p", { className: "designbase-figma-usage-status__remaining-copy" }, resolveText(translate, { key: 'remainingCopy', values: { count: remaining } }, `${remaining}회 남음 · 매일 리셋`)))) : (React.createElement("p", { className: "designbase-figma-usage-status__note" }, fallbackText(translate, 'usageUnavailable', '사용량 제한이 없습니다')))))));
1075
1083
  };
1076
1084
  UsageStatusPopover.displayName = 'UsageStatusPopover';
1077
1085
 
1078
- const FigmaFooter = ({ onLicensePageClick, paymentStatus = 'FREE', usageCount = 0, isLoading = false, showPaymentStatus = true, showUsageInfo = true, maxDailyUsage = 20, showPaymentBadge = true, showDonation = true, donationUrl = 'https://buymeacoffee.com/designbase', donationText = { key: 'donation.buy_me_a_coffee' }, showDonationText = true, donationPromptText, showLanguageSelector = true, currentLanguage = 'ko', languages, onLanguageChange, logoSrc, logoAlt = 'DesignBase', logoType = 'designbase', logoSize = 'xs', logoLinks, className, t, children, }) => {
1079
- const { t: translate } = useFigmaUiI18n(currentLanguage, t);
1080
- const resolvedShowUsageInfo = showPaymentStatus && showUsageInfo;
1081
- const resolvedShowPaymentBadge = showPaymentStatus && showPaymentBadge;
1082
- const resolvedLicensePageClick = paymentStatus === 'BETA' ? undefined : onLicensePageClick;
1083
- const hasChildren = React.Children.count(children) > 0;
1084
- const classes = clsx('designbase-figma-footer', {
1085
- 'designbase-figma-footer--has-children': hasChildren,
1086
- }, className);
1087
- return (React.createElement("footer", { className: classes },
1088
- React.createElement("div", { className: "designbase-figma-footer__wrap" },
1089
- React.createElement("div", { className: "designbase-figma-footer__left" },
1090
- React.createElement(LogoDropdown, { logoSrc: logoSrc, logoAlt: logoAlt, logoType: logoType, logoSize: logoSize, links: logoLinks, languages: languages, currentLanguage: currentLanguage, showLanguageSelector: showLanguageSelector, onLanguageChange: onLanguageChange, t: translate })),
1091
- React.createElement("div", { className: "designbase-figma-footer__right" },
1092
- (resolvedShowUsageInfo || resolvedShowPaymentBadge) && (React.createElement(UsageStatusPopover, { paymentStatus: paymentStatus, usageCount: usageCount, maxDailyUsage: maxDailyUsage, showUsage: resolvedShowUsageInfo, showPlan: resolvedShowPaymentBadge, isLoading: isLoading, onLicensePageClick: resolvedLicensePageClick, t: translate })),
1093
- showDonation && (React.createElement(DonationBadge, { donationUrl: donationUrl, text: donationText, iconType: "heart", size: "s", iconOnly: true, showPrompt: false, t: translate })))),
1094
- children));
1095
- };
1096
- FigmaFooter.displayName = 'FigmaFooter';
1097
-
1098
1086
  const FigmaToastStack = ({ toasts, position, maxToasts, onRemoveToast, }) => {
1099
1087
  const visibleToasts = [...toasts]
1100
1088
  .sort((a, b) => b.createdAt - a.createdAt)
@@ -1155,17 +1143,150 @@ const FigmaToast = ({ message, isVisible, onClose, type = 'info', duration = 300
1155
1143
  React.createElement(Toast, { toastId: "figma-toast-simple", title: message, status: type, duration: duration, showProgress: false, showClose: true, onClose: onClose })));
1156
1144
  };
1157
1145
  const useFigmaToast = () => {
1158
- const [toast, setToast] = useState({ message: '', isVisible: false });
1159
- const showToast = useCallback((message, _type = 'info') => {
1160
- setToast({ message, isVisible: true });
1161
- setTimeout(() => setToast({ message: '', isVisible: false }), 5000);
1146
+ const [toast, setToast] = useState({
1147
+ message: '',
1148
+ isVisible: false,
1149
+ type: 'info',
1150
+ });
1151
+ const timeoutRef = useRef(null);
1152
+ const showToast = useCallback((message, type = 'info') => {
1153
+ if (timeoutRef.current)
1154
+ clearTimeout(timeoutRef.current);
1155
+ setToast({ message, isVisible: true, type });
1156
+ timeoutRef.current = setTimeout(() => {
1157
+ setToast({ message: '', isVisible: false, type: 'info' });
1158
+ timeoutRef.current = null;
1159
+ }, 5000);
1162
1160
  }, []);
1163
- const toastComponent = toast.isVisible ? (React.createElement(FigmaToast, { message: toast.message, isVisible: toast.isVisible, onClose: () => setToast({ message: '', isVisible: false }), type: "info", duration: 5000 })) : null;
1161
+ useEffect(() => () => {
1162
+ if (timeoutRef.current)
1163
+ clearTimeout(timeoutRef.current);
1164
+ }, []);
1165
+ const toastComponent = toast.isVisible ? (React.createElement(FigmaToast, { message: toast.message, isVisible: toast.isVisible, onClose: () => setToast({ message: '', isVisible: false, type: 'info' }), type: toast.type, duration: 5000 })) : null;
1164
1166
  return { showToast, toastComponent };
1165
1167
  };
1166
1168
  FigmaToastProvider.displayName = 'FigmaToastProvider';
1167
1169
  FigmaToast.displayName = 'FigmaToast';
1168
1170
 
1171
+ const DEFAULT_LOW_USAGE_WARNING = { key: 'lowUsageWarning' };
1172
+ const FigmaFooter = ({ onLicensePageClick, paymentStatus = 'FREE', usageCount = 0, remainingUsage, listenForUsageMessages = true, isLoading = false, showPaymentStatus = true, showUsageInfo = true, maxDailyUsage = 20, showLowUsageWarning = true, lowUsageThresholdPercent = 10, lowUsageWarningText = DEFAULT_LOW_USAGE_WARNING, onLowUsageWarning, showPaymentBadge = true, showDonation = true, donationUrl = 'https://buymeacoffee.com/designbase', donationText = { key: 'donation.buy_me_a_coffee' }, showDonationText = true, donationPromptText, showLanguageSelector = true, currentLanguage = 'ko', languages, onLanguageChange, logoSrc, logoAlt = 'DesignBase', logoType = 'designbase', logoSize = 'xs', logoLinks, className, t, children, }) => {
1173
+ const { t: translate } = useFigmaUiI18n(currentLanguage, t);
1174
+ const { showToast, toastComponent } = useFigmaToast();
1175
+ const [liveUsage, setLiveUsage] = useState({});
1176
+ const previousRemainingRef = useRef(null);
1177
+ const hasWarnedLowUsageRef = useRef(false);
1178
+ const effectivePaymentStatus = liveUsage.paymentStatus ?? paymentStatus;
1179
+ const effectiveUsageCount = liveUsage.usageCount ?? usageCount;
1180
+ const effectiveRemainingUsage = liveUsage.remainingUsage ?? remainingUsage;
1181
+ const effectiveMaxDailyUsage = liveUsage.maxDailyUsage ?? maxDailyUsage;
1182
+ const limit = Math.max(1, Math.round(effectiveMaxDailyUsage));
1183
+ const resolvedRemaining = typeof effectiveRemainingUsage === 'number' && Number.isFinite(effectiveRemainingUsage)
1184
+ ? Math.min(limit, Math.max(0, Math.round(effectiveRemainingUsage)))
1185
+ : Math.max(0, limit - Math.min(limit, Math.max(0, Math.round(effectiveUsageCount))));
1186
+ const remainingPercent = Math.round((resolvedRemaining / limit) * 100);
1187
+ const resolvedShowUsageInfo = showPaymentStatus && (showUsageInfo || effectivePaymentStatus === 'FREE');
1188
+ const resolvedShowPaymentBadge = showPaymentStatus && showPaymentBadge;
1189
+ const resolvedLicensePageClick = effectivePaymentStatus === 'BETA' ? undefined : onLicensePageClick;
1190
+ const hasChildren = React.Children.count(children) > 0;
1191
+ useEffect(() => {
1192
+ if (!listenForUsageMessages || typeof window === 'undefined')
1193
+ return undefined;
1194
+ const handleUsageMessage = (event) => {
1195
+ const message = event.data?.pluginMessage;
1196
+ if (!message || !['initialize', 'update-plugin-status', 'usage-consume-result'].includes(message.type)) {
1197
+ return;
1198
+ }
1199
+ const nextLimit = [message.usageLimit, message.freeDailyLimit, message.maxDailyUsage]
1200
+ .find((value) => typeof value === 'number' && Number.isFinite(value) && value > 0);
1201
+ const nextUsed = [message.usageUsed, message.dailyUsage]
1202
+ .find((value) => typeof value === 'number' && Number.isFinite(value) && value >= 0);
1203
+ const nextRemaining = typeof message.remainingUsage === 'number' && Number.isFinite(message.remainingUsage)
1204
+ ? message.remainingUsage
1205
+ : typeof nextLimit === 'number' && typeof nextUsed === 'number'
1206
+ ? Math.max(0, nextLimit - nextUsed)
1207
+ : undefined;
1208
+ const nextPaymentStatus = message.paymentStatus === 'PAID'
1209
+ ? 'PAID'
1210
+ : message.paymentStatus === 'FREE' || message.paymentStatus === 'UNPAID'
1211
+ ? 'FREE'
1212
+ : undefined;
1213
+ if (nextPaymentStatus === undefined
1214
+ && nextLimit === undefined
1215
+ && nextUsed === undefined
1216
+ && nextRemaining === undefined) {
1217
+ return;
1218
+ }
1219
+ setLiveUsage((previous) => ({
1220
+ ...previous,
1221
+ ...(nextPaymentStatus ? { paymentStatus: nextPaymentStatus } : {}),
1222
+ ...(typeof nextLimit === 'number' ? { maxDailyUsage: nextLimit } : {}),
1223
+ ...(typeof nextUsed === 'number' ? { usageCount: nextUsed } : {}),
1224
+ ...(typeof nextRemaining === 'number' ? { remainingUsage: nextRemaining } : {}),
1225
+ }));
1226
+ };
1227
+ window.addEventListener('message', handleUsageMessage);
1228
+ return () => window.removeEventListener('message', handleUsageMessage);
1229
+ }, [listenForUsageMessages]);
1230
+ useEffect(() => {
1231
+ const previousRemaining = previousRemainingRef.current;
1232
+ const shouldTrack = showLowUsageWarning && effectivePaymentStatus === 'FREE' && effectiveMaxDailyUsage > 0;
1233
+ if (!shouldTrack) {
1234
+ previousRemainingRef.current = resolvedRemaining;
1235
+ hasWarnedLowUsageRef.current = false;
1236
+ return;
1237
+ }
1238
+ if (remainingPercent >= lowUsageThresholdPercent) {
1239
+ hasWarnedLowUsageRef.current = false;
1240
+ }
1241
+ else if (previousRemaining !== null
1242
+ && resolvedRemaining < previousRemaining
1243
+ && !hasWarnedLowUsageRef.current) {
1244
+ hasWarnedLowUsageRef.current = true;
1245
+ const warningText = typeof lowUsageWarningText === 'string'
1246
+ ? lowUsageWarningText
1247
+ : {
1248
+ ...lowUsageWarningText,
1249
+ values: {
1250
+ ...(lowUsageWarningText.values || {}),
1251
+ count: resolvedRemaining,
1252
+ percent: remainingPercent,
1253
+ },
1254
+ };
1255
+ onLowUsageWarning?.({
1256
+ remaining: resolvedRemaining,
1257
+ percent: remainingPercent,
1258
+ limit,
1259
+ });
1260
+ showToast(resolveText(translate, warningText, `무료 사용량이 ${resolvedRemaining}회(${remainingPercent}%) 남았습니다.`) || `무료 사용량이 ${resolvedRemaining}회(${remainingPercent}%) 남았습니다.`, 'warning');
1261
+ }
1262
+ previousRemainingRef.current = resolvedRemaining;
1263
+ }, [
1264
+ lowUsageThresholdPercent,
1265
+ lowUsageWarningText,
1266
+ effectiveMaxDailyUsage,
1267
+ effectivePaymentStatus,
1268
+ onLowUsageWarning,
1269
+ remainingPercent,
1270
+ resolvedRemaining,
1271
+ showLowUsageWarning,
1272
+ showToast,
1273
+ translate,
1274
+ ]);
1275
+ const classes = clsx('designbase-figma-footer', {
1276
+ 'designbase-figma-footer--has-children': hasChildren,
1277
+ }, className);
1278
+ return (React.createElement("footer", { className: classes },
1279
+ React.createElement("div", { className: "designbase-figma-footer__wrap" },
1280
+ React.createElement("div", { className: "designbase-figma-footer__left" },
1281
+ React.createElement(LogoDropdown, { logoSrc: logoSrc, logoAlt: logoAlt, logoType: logoType, logoSize: logoSize, links: logoLinks, languages: languages, currentLanguage: currentLanguage, showLanguageSelector: showLanguageSelector, onLanguageChange: onLanguageChange, t: translate })),
1282
+ React.createElement("div", { className: "designbase-figma-footer__right" },
1283
+ (resolvedShowUsageInfo || resolvedShowPaymentBadge) && (React.createElement(UsageStatusPopover, { paymentStatus: effectivePaymentStatus, usageCount: effectiveUsageCount, remainingUsage: effectiveRemainingUsage, maxDailyUsage: effectiveMaxDailyUsage, showUsage: resolvedShowUsageInfo, showPlan: resolvedShowPaymentBadge, isLoading: isLoading, onLicensePageClick: resolvedLicensePageClick, t: translate })),
1284
+ showDonation && (React.createElement(DonationBadge, { donationUrl: donationUrl, text: donationText, iconType: "heart", size: "s", iconOnly: true, showPrompt: false, t: translate })))),
1285
+ children,
1286
+ toastComponent));
1287
+ };
1288
+ FigmaFooter.displayName = 'FigmaFooter';
1289
+
1169
1290
  const FormWithSubmit = ({ onLicenseSubmit, disabled = false, isSubmitting = false, value = '', onValueChange, label = { key: 'form.license_key' }, placeholder = { key: 'form.license_placeholder' }, submitText = { key: 'form.submit' }, submittingText = { key: 'form.verifying' }, helperText, className, t, locale, }) => {
1170
1291
  const { t: translate } = useFigmaUiI18n(locale, t);
1171
1292
  const [inputValue, setInputValue] = useState(value);