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