diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..76d3103 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,19 @@ +import type { StorybookConfig } from "@storybook/nextjs"; + +const config: StorybookConfig = { + stories: ["../components/**/*.stories.ts?(x)"], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + ], + framework: { + name: "@storybook/nextjs", + options: {}, + }, + docs: { + autodocs: "tag", + }, + staticDirs: ["../public"], +}; +export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..4778941 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,16 @@ +import type { Preview } from "@storybook/react"; +import "../app/globals.css"; + +const preview: Preview = { + parameters: { + layout: "centered", + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/components/ui/badge.stories.tsx b/components/ui/badge.stories.tsx new file mode 100644 index 0000000..88e2b29 --- /dev/null +++ b/components/ui/badge.stories.tsx @@ -0,0 +1,178 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Badge } from "./badge"; + +const meta = { + title: "UI/Badge", + component: Badge, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + variant: { + control: "select", + options: ["default", "secondary", "destructive", "outline", "ghost", "link"], + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Default variant +export const Default: Story = { + args: { + variant: "default", + children: "Default Badge", + }, +}; + +// Secondary variant +export const Secondary: Story = { + args: { + variant: "secondary", + children: "Secondary Badge", + }, +}; + +// Destructive variant +export const Destructive: Story = { + args: { + variant: "destructive", + children: "Destructive Badge", + }, +}; + +// Outline variant +export const Outline: Story = { + args: { + variant: "outline", + children: "Outline Badge", + }, +}; + +// Ghost variant +export const Ghost: Story = { + args: { + variant: "ghost", + children: "Ghost Badge", + }, +}; + +// Link variant +export const Link: Story = { + args: { + variant: "link", + children: "Link Badge", + }, +}; + +// All variants showcase +export const AllVariants: Story = { + render: () => ( +
+ Default + Secondary + Destructive + Outline + Ghost + Link +
+ ), +}; + +// With different text lengths +export const LongText: Story = { + args: { + variant: "default", + children: "This is a longer badge text", + }, +}; + +export const ShortText: Story = { + args: { + variant: "default", + children: "New", + }, +}; + +// Status badges examples +export const StatusActive: Story = { + args: { + variant: "default", + children: "Active", + }, +}; + +export const StatusInactive: Story = { + args: { + variant: "outline", + children: "Inactive", + }, +}; + +export const StatusError: Story = { + args: { + variant: "destructive", + children: "Error", + }, +}; + +export const StatusWarning: Story = { + args: { + variant: "secondary", + children: "Warning", + }, +}; + +// Tag-like badges +export const TagDefault: Story = { + args: { + variant: "default", + children: "React", + }, +}; + +export const TagSecondary: Story = { + args: { + variant: "secondary", + children: "TypeScript", + }, +}; + +export const TagOutline: Story = { + args: { + variant: "outline", + children: "CSS", + }, +}; + +// Combination showcase +export const VariantShowcase: Story = { + render: () => ( +
+
+

Variants

+
+ Default + Secondary + Destructive + Outline + Ghost + Link +
+
+
+

Common Use Cases

+
+ Featured + New + Urgent + Pending + Draft + Learn more +
+
+
+ ), +}; diff --git a/components/ui/button.stories.tsx b/components/ui/button.stories.tsx new file mode 100644 index 0000000..fe138ef --- /dev/null +++ b/components/ui/button.stories.tsx @@ -0,0 +1,180 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { Button } from "./button"; + +const meta = { + title: "UI/Button", + component: Button, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: { + variant: { + control: "select", + options: ["default", "outline", "secondary", "ghost", "destructive", "link"], + }, + size: { + control: "select", + options: ["default", "xs", "sm", "lg", "icon", "icon-xs", "icon-sm", "icon-lg"], + }, + disabled: { + control: "boolean", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Default variant stories +export const Default: Story = { + args: { + variant: "default", + size: "default", + children: "Click me", + }, +}; + +export const DefaultSmall: Story = { + args: { + variant: "default", + size: "sm", + children: "Small button", + }, +}; + +export const DefaultLarge: Story = { + args: { + variant: "default", + size: "lg", + children: "Large button", + }, +}; + +// Ghost variant stories +export const Ghost: Story = { + args: { + variant: "ghost", + size: "default", + children: "Ghost button", + }, +}; + +export const GhostSmall: Story = { + args: { + variant: "ghost", + size: "sm", + children: "Small ghost", + }, +}; + +export const GhostLarge: Story = { + args: { + variant: "ghost", + size: "lg", + children: "Large ghost", + }, +}; + +// Secondary variant stories +export const Secondary: Story = { + args: { + variant: "secondary", + size: "default", + children: "Secondary button", + }, +}; + +export const SecondarySmall: Story = { + args: { + variant: "secondary", + size: "sm", + children: "Small secondary", + }, +}; + +export const SecondaryLarge: Story = { + args: { + variant: "secondary", + size: "lg", + children: "Large secondary", + }, +}; + +// Destructive variant stories +export const Destructive: Story = { + args: { + variant: "destructive", + size: "default", + children: "Delete", + }, +}; + +export const DestructiveSmall: Story = { + args: { + variant: "destructive", + size: "sm", + children: "Delete", + }, +}; + +export const DestructiveLarge: Story = { + args: { + variant: "destructive", + size: "lg", + children: "Delete", + }, +}; + +// Icon size stories +export const IconDefault: Story = { + args: { + variant: "default", + size: "icon", + children: "🔍", + }, +}; + +export const IconSmall: Story = { + args: { + variant: "default", + size: "icon-sm", + children: "📝", + }, +}; + +export const IconLarge: Story = { + args: { + variant: "default", + size: "icon-lg", + children: "⚙️", + }, +}; + +// Disabled state +export const Disabled: Story = { + args: { + variant: "default", + size: "default", + children: "Disabled button", + disabled: true, + }, +}; + +export const DisabledGhost: Story = { + args: { + variant: "ghost", + size: "default", + children: "Disabled ghost", + disabled: true, + }, +}; + +export const DisabledDestructive: Story = { + args: { + variant: "destructive", + size: "default", + children: "Disabled delete", + disabled: true, + }, +}; diff --git a/contracts/streaming/src/lib.rs b/contracts/streaming/src/lib.rs index 043a491..bc3393f 100644 --- a/contracts/streaming/src/lib.rs +++ b/contracts/streaming/src/lib.rs @@ -323,6 +323,7 @@ pub struct StreamTransferEvent { pub stream_id: u64, pub old_recipient: Address, pub new_recipient: Address, + pub timestamp: u64, } /// Emitted when the sender adds additional funds to an active stream via `top_up`. @@ -336,6 +337,7 @@ pub struct TopUpEvent { pub additional_amount: i128, pub new_deposited_amount: i128, pub new_amount_per_second: i128, + pub timestamp: u64, } /// Emitted when anyone calls `bump_stream` to extend a stream's ledger TTL. @@ -419,12 +421,7 @@ impl StreamingContract { /// Pause all write operations (admin only). pub fn pause(env: Env) -> Result<(), StreamError> { - let admin: Address = env - .storage() - .instance() - .get(&DataKey::Admin) - .ok_or(StreamError::NotInitialized)?; - admin.require_auth(); + Self::require_admin(&env)?; env.storage().instance().set(&DataKey::Paused, &true); env.storage() @@ -440,12 +437,7 @@ impl StreamingContract { /// Unpause all write operations (admin only). pub fn unpause(env: Env) -> Result<(), StreamError> { - let admin: Address = env - .storage() - .instance() - .get(&DataKey::Admin) - .ok_or(StreamError::NotInitialized)?; - admin.require_auth(); + Self::require_admin(&env)?; env.storage().instance().set(&DataKey::Paused, &false); env.storage() @@ -483,12 +475,7 @@ impl StreamingContract { /// Post-upgrade data migration hook. Call this after an upgrade to /// migrate storage layouts. pub fn migrate(env: Env) -> Result<(), StreamError> { - let admin: Address = env - .storage() - .instance() - .get(&DataKey::Admin) - .ok_or(StreamError::NotInitialized)?; - admin.require_auth(); + Self::require_admin(&env)?; // By default, unfreeze after wasm upgrade. env.storage().instance().set(&DataKey::Paused, &false); @@ -497,6 +484,20 @@ impl StreamingContract { // ─── Helpers ────────────────────────────────────────────────────────────── + /// Extract and validate the admin address, ensuring it is initialized and authorized. + /// + /// This helper consolidates the repeated pattern in `pause`, `unpause`, and `migrate` + /// of retrieving the admin from storage and requiring its authorization. + fn require_admin(env: &Env) -> Result { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(StreamError::NotInitialized)?; + admin.require_auth(); + Ok(admin) + } + fn require_not_paused(env: &Env) -> Result<(), StreamError> { let paused: bool = env .storage() @@ -808,10 +809,12 @@ impl StreamingContract { .persistent() .remove(&DataKey::Delegate(stream_id)); + let timestamp = env.ledger().timestamp(); StreamTransferEvent { stream_id, old_recipient, new_recipient, + timestamp, } .publish(&env); Self::extend_stream_ttl(&env, stream_id); @@ -921,11 +924,13 @@ impl StreamingContract { Self::extend_stream_ttl(&env, stream_id); + let timestamp = env.ledger().timestamp(); TopUpEvent { stream_id, additional_amount, new_deposited_amount: stream.deposited_amount, new_amount_per_second: stream.amount_per_second, + timestamp, } .publish(&env); diff --git a/package.json b/package.json index 44edd05..acfb274 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "test:e2e": "playwright test", "test:e2e:update": "playwright test -u", "prepare": "husky", - "analyze": "ANALYZE=true next build --webpack" + "analyze": "ANALYZE=true next build --webpack", + "storybook": "storybook dev -p 6006", + "storybook:build": "storybook build" }, "dependencies": { "@albedo-link/intent": "^0.13.0", @@ -47,6 +49,11 @@ "@next/bundle-analyzer": "^15.0.0", "@next/eslint-plugin-next": "^16.2.9", "@playwright/test": "^1.61.0", + "@storybook/addon-essentials": "^8.0.0", + "@storybook/addon-interactions": "^8.0.0", + "@storybook/addon-links": "^8.0.0", + "@storybook/nextjs": "^8.0.0", + "@storybook/react": "^8.0.0", "@tailwindcss/postcss": "^4.2.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", @@ -65,6 +72,7 @@ "lint-staged": "^15.0.0", "postcss": "^8.5", "prettier": "^3.0.0", + "storybook": "^8.0.0", "tailwindcss": "^4.2.0", "typescript": "5.7.3", "vitest": "^2.1.9"